Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/947/head
Sebastian 4 years ago
parent
commit
efa1d48b0c
  1. 1111
      backend/i18n/frontend_pt.json
  2. 6
      backend/i18n/package-lock.json
  3. 399
      backend/i18n/source/backend_pt.json
  4. 1111
      backend/i18n/source/frontend_pt.json
  5. 2
      backend/i18n/translator/Squidex.Translator/Commands.cs
  6. 13
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  7. 14
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  9. 24
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs
  10. 8
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  11. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs
  12. 1276
      backend/src/Squidex.Shared/Texts.pt.resx
  13. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  14. 2
      backend/src/Squidex/Config/Web/WebExtensions.cs
  15. 51
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  16. 161
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  17. 2
      backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs
  18. 5
      backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs
  19. 8
      backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  20. 195
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs
  21. 144
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs
  22. 277
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  23. 12
      backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  24. 4
      backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
  25. 44
      backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs
  26. 16
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  27. 33
      frontend/package-lock.json
  28. 2
      frontend/package.json
  29. 22
      frontend/src/app/_theme.html
  30. 2
      frontend/src/app/features/administration/pages/users/user-page.component.html
  31. 2
      frontend/src/app/features/assets/pages/asset-tag-dialog.component.html
  32. 2
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  33. 11
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  34. 17
      frontend/src/app/features/content/shared/forms/iframe-editor.component.html
  35. 11
      frontend/src/app/features/content/shared/forms/iframe-editor.component.scss
  36. 43
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  37. 2
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
  38. 2
      frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.html
  39. 6
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  40. 2
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html
  41. 6
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html
  42. 2
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.html
  43. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html
  44. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html
  45. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.html
  46. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html
  47. 8
      frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.html
  48. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.html
  49. 4
      frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html
  50. 2
      frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html
  51. 2
      frontend/src/app/features/schemas/pages/schemas/schema-form.component.html
  52. 2
      frontend/src/app/features/settings/pages/clients/client-add-form.component.html
  53. 6
      frontend/src/app/features/settings/pages/more/more-page.component.html
  54. 2
      frontend/src/app/features/settings/pages/roles/role-add-form.component.html
  55. 10
      frontend/src/app/features/settings/pages/settings/settings-page.component.html
  56. 2
      frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.html
  57. 2
      frontend/src/app/features/teams/pages/more/more-page.component.html
  58. 2
      frontend/src/app/framework/angular/forms/editable-title.component.html
  59. 2
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.html
  60. 4
      frontend/src/app/framework/angular/forms/editors/date-time-editor.component.html
  61. 2
      frontend/src/app/framework/angular/forms/editors/dropdown.component.html
  62. 2
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.html
  63. 2
      frontend/src/app/shared/components/app-form.component.html
  64. 8
      frontend/src/app/shared/components/assets/asset-dialog.component.html
  65. 2
      frontend/src/app/shared/components/assets/asset-folder-dialog.component.html
  66. 0
      frontend/src/app/shared/components/assets/asset-selector.component.html
  67. 0
      frontend/src/app/shared/components/assets/asset-selector.component.scss
  68. 8
      frontend/src/app/shared/components/assets/asset-selector.component.ts
  69. 4
      frontend/src/app/shared/components/contents/content-value-editor.component.html
  70. 2
      frontend/src/app/shared/components/forms/geolocation-editor.component.html
  71. 4
      frontend/src/app/shared/components/forms/markdown-editor.component.html
  72. 4
      frontend/src/app/shared/components/forms/rich-editor.component.html
  73. 11
      frontend/src/app/shared/components/references/content-selector.component.ts
  74. 2
      frontend/src/app/shared/components/references/reference-input.component.html
  75. 4
      frontend/src/app/shared/components/search/queries/filter-comparison.component.html
  76. 2
      frontend/src/app/shared/components/search/search-form.component.html
  77. 2
      frontend/src/app/shared/components/team-form.component.html
  78. 2
      frontend/src/app/shared/declarations.ts
  79. 6
      frontend/src/app/shared/module.ts
  80. 3
      frontend/src/app/shared/state/ui-languages.ts
  81. 10
      frontend/src/app/theme/_bootstrap.scss
  82. 1034
      frontend/src/app/theme/icomoon/demo.html

1111
backend/i18n/frontend_pt.json

File diff suppressed because it is too large

6
backend/i18n/package-lock.json

@ -0,0 +1,6 @@
{
"name": "i18n",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

399
backend/i18n/source/backend_pt.json

@ -0,0 +1,399 @@
{
"annotations_AbsoluteUrl": "O campo {name|lower} deve ser uma URL absoluta.",
"annotations_Compare": "O campo {name|lower} deve ser o mesmo que {1.}.",
"annotations_EmailAddress": "O campo {name|lower} não é um endereço de e-mail válido.",
"annotations_Range": "O campo {name|lower} deve estar entre {min} e {max}.",
"annotations_RegularExpression": "O campo não é.",
"annotations_Required": "O campo {name|lower} é necessário.",
"annotations_StringLength": "O campo '{name|lower}' deve ser um texto com tamanho máximo de {max}.",
"annotations_StringLengthMinimum": "O campo '{name|lower}' deve ser um texto com tamanho mínimo de {min} e no máximo de {max}.",
"apps.clients.idAlreadyExists": "Um cliente com o mesmo id já existe.",
"apps.contributors.cannotChangeYourself": "Você não pode mudar seu próprio papel.",
"apps.contributors.maxReached": "Alcançou o tamanho máximo de colaboradors disponiveis para o seu plano.",
"apps.contributors.onlyOneOwner": "Não é possível remover o unico dono.",
"apps.languages.fallbackNotFound": "App não tem uma língua de reserva '{fallback}'.",
"apps.languages.languageAlreadyAdded": "Linguagem foi adicionada.",
"apps.languages.masterLanguageNoFallbacks": "A linguagem principal não pode ter linguagens de reserva.",
"apps.languages.masterLanguageNotOptional": "A linguagem principal não pode ser feita opcional.",
"apps.languages.masterLanguageNotRemovable": "A linguagem principal não pode ser removida.",
"apps.maximumTotalReached": "Você não pode criar mais aplicativos. Entre em contato com o suporte para remover essa restrição da sua conta.",
"apps.nameAlreadyExists": "Já existe uma app com esse nome.",
"apps.notImage": "Ficheiro não é uma imagem",
"apps.plans.assignedToTeam": "Plano é gerido por uma equipa.",
"apps.plans.notFound": "Um plano com este id não existe.",
"apps.plans.notPlanOwner": "Plano só pode ser alterado pelo utilizador que configurou inicialmente.",
"apps.roles.defaultRoleNotRemovable": "Não pode remover o grupo pré definido.",
"apps.roles.defaultRoleNotUpdateable": "Não pode alterar o grupo pré definido.",
"apps.roles.nameAlreadyExists": "Um grupo com o mesmo nome já existe.",
"apps.roles.usedRoleByClientsNotRemovable": "Não pode remover grupo quando tem um cliente associado.",
"apps.roles.usedRoleByContributorsNotRemovable": "Nãopode remover o grupo quando tem um colaborador associado.",
"apps.transfer.planAssigned": "A inscrição deve ser cancelada antes que o aplicativo possa ser transferido.",
"apps.transfer.teamNotFound": "A equipa não existe.",
"assets.folderNotFound": "A pasta de ficheiros não existe.",
"assets.folderRecursion": "Não é possível adicionar pasta à sua própria criança.",
"assets.maxSizeReached": "Você alcançou o tamanho máximo de ficheiros.",
"assets.referenced": "Os ficheiros são referenciados por um conteúdo e não podem ser excluídos.",
"backups.alreadyRunning": "Já se encontra um processo de backup em processamento.",
"backups.maxReached": "Não pode ter mais de {max} backups.",
"backups.restoreRunning": "Um processamento de restauro já se encontra a correr.",
"comments.noPermissions": "Só pode aceder as suas notificações.",
"comments.notUserComment": "Comentário foi criado por outro utilizador.",
"common.action": "Acção",
"common.aspectHeight": "Altura aspecto",
"common.aspectWidth": "Largura aspecto",
"common.calculatedDefaultValue": "Calculado o valor por defeito",
"common.clientd": "ID do cliente",
"common.clientId": "ID do cliente",
"common.clientSecret": "Segredo do cliente",
"common.contentType": "Tipo conteúdo",
"common.contributorId": "ID or email do colaborador",
"common.critical": "Critico",
"common.data": "Data",
"common.defaultValue": "Valor por defeito",
"common.displayName": "Nome apresentação",
"common.documentation": "Documentação",
"common.editor": "Editor",
"common.email": "Email",
"common.errorNoPermission": "Você não tem a permissão necessária.",
"common.field": "Campo",
"common.fieldIds": "IDs de campo",
"common.fieldName": "Nome do campo",
"common.file": "Ficheiro",
"common.folderName": "Nome da pasta",
"common.fullTextNotSupported": "Cláusula de busca de consulta não suportada.",
"common.httpContentTypeNotDefined": "O tipo de conteúdo do ficheiro não está definido.",
"common.httpFileNameNotDefined": "O nome do ficheiro não é definido.",
"common.httpInvalidRequest": "O modelo não é válido.",
"common.httpInvalidRequestFormat": "Pedido corpo tem um formato inválido.",
"common.httpOnlyAsUser": "Não permitido para clientes.",
"common.httpValidationError": "Erros de validação",
"common.initialStep": "Passo Inicial",
"common.jsError": "Erro ao executar script javascript: {message}",
"common.jsNotAllowed": "Script proibiu a operação.",
"common.jsParseError": "Erro ao executar script javascript com erro de sintaxe: {message}",
"common.jsRejected": "Script rejeitou a operação.",
"common.language": "Código da língua",
"common.login": "Entrar",
"common.logout": "Sair",
"common.maxCharacters": "Caracteres máximos",
"common.maxHeight": "Altura Máx",
"common.maxItems": "Items Máx",
"common.maxLength": "Comprimento Máx",
"common.maxSize": "Tamanho Máx",
"common.maxValue": "Valor Máx",
"common.maxWidth": "Largura Máx",
"common.maxWords": "Palavras Máx",
"common.minCharacters": "Min characters",
"common.minHeight": "Altura Min",
"common.minItems": "Items Min",
"common.minLength": "Comprimento Min",
"common.minSize": "Tamanho Min",
"common.minValue": "Valor Min",
"common.minWidth": "Largura Min",
"common.minWords": "Palavras Min",
"common.name": "Nome",
"common.notFoundValue": "- não encontrado -",
"common.numDays": "Num dias",
"common.odataFailure": "Falha ao analisar consulta: {message}, para a consulta: {odata}",
"common.odataFilterNotValid": "OData $filter clausula é inválida: {message}",
"common.odataNotSupported": "OData operação não é suportada pela consulta: {odata}",
"common.odataSearchNotValid": "OData $search clausula é invalida: {message}",
"common.oldPassword": "Password antiga",
"common.other": "Outro",
"common.partitioning": "Particionamento",
"common.password": "Password",
"common.passwordConfirm": "Confirmar",
"common.pattern": "Padrão",
"common.permissions": "Permissões",
"common.planId": "ID Plano",
"common.previewUrls": "pre-visualizar URLs",
"common.product": "Squidex Headless CMS",
"common.properties": "Propriedades",
"common.property": "Propriedade",
"common.readonlyMode": "App está em modo de leitura de momento.",
"common.remove": "Remover",
"common.resultTooLarge": "Resultado é muito grande para poder ser retornado. Use o parametro $take para reduzir o numero de items.",
"common.role": "Grupo",
"common.save": "Gravar",
"common.schemaId": "ID Esquema",
"common.signup": "Registar",
"common.success": "Sucesso",
"common.text": "Texto",
"common.trigger": "Gatilho",
"common.warning": "Aviso",
"common.workflow": "Fluxo de trabalho",
"common.workflowStep": "Passo",
"common.workflowTransition": "Trancisão",
"contents.bulkInsertQueryNotUnique": "Mais do que um conteúdo corresponde à consulta.",
"contents.componentNotCreatable": "O conteúdo do componente não pode ser criado.",
"contents.draftNotCreateForUnpublished": "Só pode criar uma nova ver~sao quando o conteúdo estiver publicado.",
"contents.draftToDeleteNotFound": "Não existe nada para remover.",
"contents.invalidAllQuery": "Os IDs ou o intervalo de programação devem ser definidos.",
"contents.invalidArrayOfObjects": "Json type inválido, esperado uma lista of objectos.",
"contents.invalidArrayOfStrings": "Json type inválido, esperado uma lista de textos.",
"contents.invalidBoolean": "Json type inválido, esperado um booleano.",
"contents.invalidComponentNoObject": "Json object inválido, esperado um object com campo 'schemaId'.",
"contents.invalidComponentNoType": "Componente inválido. Nenhum campo 'schemaId' encontrado.",
"contents.invalidComponentUnknownSchema": "Componente inválido. Esquema não encontrado.",
"contents.invalidGeolocation": "Json type inválido, esperado object de latitude/longitude.",
"contents.invalidGeolocationLatitude": "Latitude deve estár entre -90 e 90.",
"contents.invalidGeolocationLongitude": "Longitude deve estár entre -180 e 180.",
"contents.invalidNumber": "Json type inválido, esperado número.",
"contents.invalidString": "Json type inválido, esperado texto.",
"contents.listReferences": "{count} Referencia(s)",
"contents.referenced": "O conteúdo é referenciado por outro conteúdo e não pode ser removido ou não publicado.",
"contents.schemaNotPublished": "Esquema não está publicada.",
"contents.singletonNotChangeable": "Conteúdo único não pode ser actualizado.",
"contents.singletonNotCreatable": "Conteúdo único não pode ser criado.",
"contents.singletonNotDeletable": "Conteúdo único não pode ser removido.",
"contents.statusNotValid": "Status não está definido no fluxo de trabalho.",
"contents.statusTransitionNotAllowed": "Não foi possível alterar o estado {oldStatus} para {newStatus}.",
"contents.validation.aspectRatio": "Deve ter a proporção {width}:{height}.",
"contents.validation.assetNotFound": "Id {id} não encontrado.",
"contents.validation.assetType": "Não pertence ao tipo permitido: {type}.",
"contents.validation.between": "Deve ter entre {min} e {max}.",
"contents.validation.characterCount": "Deve ter exatamente {count} caractere(es).",
"contents.validation.charactersBetween": "Deve ter entre {min} e {max} caractere(es).",
"contents.validation.duplicates": "Não pode ter duplicados.",
"contents.validation.error": "Validação falhou com um erro interno.",
"contents.validation.exactValue": "Deve ter exatamente {value}.",
"contents.validation.extension": "Extensão não é permitida.",
"contents.validation.invalid": "Valor inválido.",
"contents.validation.itemCount": "Deve ter exatamente {count} item(s).",
"contents.validation.itemCountBetween": "Deve ter exatamente {min} e {max} item(s).",
"contents.validation.max": "Deve ser igual ou menor que {max}.",
"contents.validation.maxCharacters": "Não pode ter mais que {max} caracter(s).",
"contents.validation.maximumHeight": "Altura {height}px deve ser menos que {max}px.",
"contents.validation.maximumSize": "Tamanho {size} deve ser menos que {max}.",
"contents.validation.maximumWidth": "Largura {width}px deve ser menos que {max}px.",
"contents.validation.maxItems": "Não pode ter mais que {max} item(s).",
"contents.validation.maxLength": "Não pode ter mais que {max} caracter(s).",
"contents.validation.maxWords": "Não pode ter mais que {max} palavras(s).",
"contents.validation.min": "Deve ser maior ou igual que {min}.",
"contents.validation.minimumHeight": "Altura {height}px deve ser maior que {min}px.",
"contents.validation.minimumSize": "Tamanho of {size} deve ser maior que {min}.",
"contents.validation.minimumWidth": "Largura {width}px deve ser maior que {min}px.",
"contents.validation.minItems": "Deve ter no mínimo {min} item(s).",
"contents.validation.minLength": "Deve ter no mínimo {min} caracter(s).",
"contents.validation.minNormalCharacters": "Deve ter no mínimo {min} caracter(s).",
"contents.validation.minWords": "Deve ter no mínimo {min} word(s).",
"contents.validation.mustBeEmpty": "Value must not be defined.",
"contents.validation.normalCharacterCount": "Deve ter exactamente {count} caracter(s).",
"contents.validation.normalCharactersBetween": "Deve ter entre {min} e {max} caracter(s).",
"contents.validation.notAllowed": "Valor não é permitido.",
"contents.validation.pattern": "Deve seguir o padrão.",
"contents.validation.referenceNotFound": "Referencia '{id}' não encontrada.",
"contents.validation.referenceToInvalidSchema": "Referencia '{id}' com esquema inválido.",
"contents.validation.regexTooSlow": "Regex demasiado lento.",
"contents.validation.required": "Campo obrigatório.",
"contents.validation.unique": "Já existe um conteúdo com o mesmo valor.",
"contents.validation.uniqueObjectValues": "Não pode conter items com campos duplicados '{field}'.",
"contents.validation.unknownField": "{fieldType} não conhecido.",
"contents.validation.wordCount": "Deve ter exatamente {count} palavra(s).",
"contents.validation.wordsBetween": "Deve ter entre {min} e {max} palavra(s).",
"contents.workflowErrorUpdate": "O fluxo de trabalho não permite actualizar o estado {status}",
"dotnet_identity_DefaultEror": "Um erro desconhecido ocorreu.",
"dotnet_identity_DuplicateEmail": "Email já existe.",
"dotnet_identity_DuplicateUserName": "Email já está registado noutro utilizador. Pode actualizar o seu login externo para a sua conta se for a sua página de perfil.",
"dotnet_identity_InvalidEmail": "Email é inválido.",
"dotnet_identity_InvalidUserName": "Nome de utilizador '{0}' é invlálido, só pode conter letras ou digitos.",
"dotnet_identity_LoginAlreadyAssociated": "Um utilizador com este login já existe.",
"dotnet_identity_PasswordMismatch": "Password incorrecta.",
"dotnet_identity_PasswordRequiresDigit": "Passwords devem ter um digito ('0'-'9').",
"dotnet_identity_PasswordRequiresLower": "Passwords devem ter um letra minusculo ('a'-'z').",
"dotnet_identity_PasswordRequiresNonAlphanumeric": "Passwords devem ter um caracter não alfa numerico.",
"dotnet_identity_PasswordRequiresUniqueChars": "Passwords deve ter no mínimo {0} caracteres diferentes.",
"dotnet_identity_PasswordRequiresUpper": "Passwords devem ter um caracter maiusculo ('A'-'Z').",
"dotnet_identity_PasswordTooShort": "Passwords é muito curta.",
"dotnet_identity_PwnedError": "Esta password já apareceu numa violação de dados e não deve ser usada. Se já usou antes nalgum lado, por favor altere!",
"dotnet_identity_UserLockedOut": "User está bloqueado.",
"exceptions.domainObjectConflict": "Entidade ({id}) já existe.",
"exceptions.domainObjectDeleted": "Entidade ({id}) foi removida.",
"exceptions.domainObjectNotFound": "Entidade ({id}) não existe.",
"exceptions.domainObjectVersion": "Entidade ({id}) versão {expectedVersion} não encontrada, mas sim {currentVersion}.",
"history.apps.assetScriptsConfigured": "Scripts de ficheiros actualizados",
"history.apps.clientAdded": "cliente {[Id]} adicionado na app",
"history.apps.clientRevoked": "cliente {[Id]} revogado ",
"history.apps.clientUpdated": "cliente {[Id]} actualizado",
"history.apps.contributoreAssigned": "associado {user:[Contributor]} a {[Role]}",
"history.apps.contributoreRemoved": "removido {user:[Contributor]} da app",
"history.apps.created": "app criada.",
"history.apps.imageRemoved": "imagem da app removida",
"history.apps.imageUploaded": "carregada nova imagem da app",
"history.apps.languagedAdded": "adicionada língua {[Language]}",
"history.apps.languagedRemoved": "removida língua {[Language]}",
"history.apps.languagedSetToMaster": "alterada a língua principal para {[Language]}",
"history.apps.languagedUpdated": "actualizada língua {[Language]}",
"history.apps.planChanged": "alterado o plano para {[Plan]}",
"history.apps.planReset": "plano redifinido",
"history.apps.roleAdded": "adicionado grupo {[Name]}",
"history.apps.roleDeleted": "removido grupo {[Name]}",
"history.apps.roleUpdated": "actualizado grupo {[Name]}",
"history.apps.settingsUpdated": "Configurações UI actualizadas",
"history.apps.transfered": "actualizada app ao cliente",
"history.apps.updated": "actualizadas configurações gerais",
"history.apps.workflowAdded": "adicionado fluxo de trabalho {[Name]}.",
"history.apps.workflowDeleted": "fluxo de trabalho removido.",
"history.apps.workflowUpdated": "fluxo de trabalho actualizado.",
"history.assets.replaced": "ficheiro substituido.",
"history.assets.updated": "ficheiro actualizado.",
"history.assets.uploaded": "ficheiro carregado.",
"history.contents.created": "conteúdo criado {[Schema]}.",
"history.contents.deleted": "conteúdo removido {[Schema]}.",
"history.contents.draftCreated": "creado novo rascunho.",
"history.contents.draftDeleted": "removido rascunho.",
"history.contents.scheduleCompleted": "marcada alteração de estado de {[Schema]} para {[Status]}.",
"history.contents.scheduleFailed": "erro ao marcar alteração de estado para conteúdo {[Schema]}.",
"history.contents.updated": "conteúdo {[Schema]} actualizado.",
"history.schemas.created": "esquema {[Name]} criado.",
"history.schemas.deleted": "esquema {[Name]} removido.",
"history.schemas.fieldAdded": "adicionado campo {[Field]} no esquema {[Name]}.",
"history.schemas.fieldDeleted": "removido campo {[Field]} do esquema {[Name]}.",
"history.schemas.fieldDisabled": "desabilitado campo {[Field]} no esquema {[Name]}.",
"history.schemas.fieldHidden": "tem campo {[Field]} escondido no esquema {[Name]}.",
"history.schemas.fieldLocked": "campo {[Field]} bloqueado no esquema {[Name]}.",
"history.schemas.fieldShown": "mostar campo {[Field]} no esquema {[Name]}.",
"history.schemas.fieldsReordered": "re-ordenar campos no esquema {[Name]}.",
"history.schemas.fieldUpdated": "campo {[Field]} actualizado no esquema {[Name]}.",
"history.schemas.published": "esquema publicado {[Name]}.",
"history.schemas.scriptsConfigured": "configurado script no esquema {[Name]}.",
"history.schemas.unpublished": "despublicado esquema {[Name]}.",
"history.schemas.updated": "esquema {[Name]} actualizado.",
"history.statusChanged": "alterado estado {[Schema]} para {[Status]}.",
"history.teams.contributoreAssigned": "associado {user:[Contributor]} ao {[Role]}.",
"history.teams.contributoreRemoved": "removido {user:[Contributor]} da equipa.",
"history.teams.created": "foi criada a equipa {[Name]}.",
"history.teams.planChanged": "alterado plano para {[Plan]}.",
"history.teams.planReset": "plano redifinido.",
"history.teams.updated": "actualizadas configurações gerais e renomeado para {[Name]}.",
"login.githubPrivateEmail": "O seu Email é privado no Github. Altere para publico no Github e tente novamente.",
"rules.ruleAlreadyRunning": "Outra regra já se encontra a correr.",
"schemas.dateTimeCalculatedDefaultAndDefaultError": "Valor por defeito calculado e valor por defeito não podem ser usado em conjunto.",
"schemas.duplicateFieldName": "Campo '{field}' foi adicionado em duplicado.",
"schemas.fieldCannotBeUIField": "Campo não pode ser um campo UI.",
"schemas.fieldIsLocked": "Campo do esquema está bloqueado.",
"schemas.fieldNameAlreadyExists": "Um campo com o mesmo nome já existe.",
"schemas.fieldNotInSchema": "CAmpo não é parte do esquema.",
"schemas.fieldsNotCovered": "Ids do campo não cobrem todos os campos.",
"schemas.nameAlreadyExists": "Um esquema com o mesmo nome já existe.",
"schemas.noPermission": "Não tem permissões para este esquema.",
"schemas.notFoundId": "Esquema {id} não existe.",
"schemas.number.inlineEditorError": "Não é permitido alteração em linha no Radio editor.",
"schemas.onlyArraysHaveNested": "So campos lista podem ter campos aninhados.",
"schemas.onylArraysInRoot": "Campo aninhado não pode ser lista de campos.",
"schemas.references.resolveError": "Só pode resolver uma referencia quando MaxItems é 1.",
"schemas.string.inlineEditorError": "Alteração em linha só é permitida em dropdowns, slugs e input fields.",
"schemas.stringEditorsNeedAllowedValuesError": "Radio buttons ou listas dropdown precisam de campos disponiveis.",
"schemas.tags.editorNeedsAllowedValues": "Checkboxes ou listas dropdown precisam de campos disponiveis.",
"schemas.uiFieldCannotBeDisabled": "Campo UI não pode ser desabilitado.",
"schemas.uiFieldCannotBeEnabled": "Campo UI não pode ser habilitado.",
"schemas.uiFieldCannotBeHidden": "Campo UI não pode ser escondido.",
"schemas.uiFieldCannotBeShown": "Campo UI não pode ser mostrado.",
"search.contentResult": "{name} conteúdos",
"search.contentsResult": "{name} conteúdos",
"search.schemaResult": "{name} Esquema",
"setup.createUser.button": "Criado utilizador",
"setup.createUser.confirmPassword": "Confirmar",
"setup.createUser.failure": "Nem autenticação por password or usando um provedor externo como o Google está configurado. Por favor verifique as suas configurações e logs.",
"setup.createUser.headline": "Administrator",
"setup.createUser.headlineCreate": "Criar administrador",
"setup.createUser.loginHint": "Configurou pelo menos um provedor de autenticação externo como o Google. Faça o login novamente para tornar-se administrador.",
"setup.createUser.loginLink": "Ir para login.",
"setup.createUser.separator": "OU",
"setup.headline": "Instalação",
"setup.hint": "Está a ver este ecrã porque não existe ainda nenhum utilizador. Depois de criar um utilizador, este setup fica desabilitado.",
"setup.madeBy": "Orgulhosamente feito por",
"setup.madeByCopyright": "Sebastian Stehle e Contribuintes, 2016-2022",
"setup.ruleAppCreation.warningAdmins": "Com a sua configuração, apenas administradores podem criar novas apps. Se pretende alterar, altere esta variavel de sistema <code>UI__ONLYADMINSCANCREATEAPPS=false</code>.",
"setup.ruleAppCreation.warningAll": "Com a sua configuração, qualquer utilizador pode criar apps. Se pretende alterar, altere esta variavel de sistema <code>UI__ONLYADMINSCANCREATEAPPS=true</code>.",
"setup.ruleFolder.warning": "Está a usar <strong>armazenamento de ficheiros</strong> onde todos ficheiros são gravados no sistema de ficheiros local. Por favor lembrar incluir a pasta de ficheiros dentro da sua estratégia de backup e mapear para um volume, se estiver a usar docker.",
"setup.ruleFtp.warning": "Está a usar <strong>armazenamento de ficheiros com FTP</strong>. Não é recomendado usar este tipo devido a problemas de performance.",
"setup.ruleHttps.failure": "Não está a aceder a este site via https. Se este aviso não está correcto então Squidex não pode detectar o modo https, provavelmente a sua instância está por tras de uma reverse proxy como nginx. Confirme que http headers are forwarded properly, via the <code>X-Forwarded-*</code> headers.",
"setup.ruleHttps.success": "Parabéns, está aceder Squidex por detrás de uma conexão segura (https).",
"setup.rules.headline": "Lista de verificação do sistema",
"setup.ruleTeamCreation.warningAdmins": "Com a sua configuração, apenas administradores podem criar novas equipas. Se pretende alterar, altere esta variavel de sistema <code>UI__ONLYADMINSCANCREATETEAMS=false</code>.",
"setup.ruleTeamCreation.warningAll": "Com a sua configuração, qualquer utilizador pode criar equipas. Se pretende alterar, altere esta variavel de sistema <code>UI__ONLYADMINSCANCREATETEAMS=true</code>.",
"setup.ruleUrl.failure": "Deve aceder ao Squidex so atraves de um URL canónico e configurar este URL na variavel de ambiente <code>URLS__BASEURL</code>. A base de URL actual <code>{actual}</code> não coincide com <code>{configured}</code>. Esta variavel deve apontar para um URL publico onde a sua instancia Squidex está disponível.",
"setup.ruleUrl.success": "Parabéns, a variavel de ambiente <code>URLS__BASEURL</code> environment está configurada correctamente. Esta variavel deve apontar para um URL publico no qual a sua instância de Suidex está disponível.",
"setup.title": "Instalação",
"users.accessDenied.text": "Operação não permitida, a sua conta pode estár bloqueada.",
"users.accessDenied.title": "Accesso bloqueado",
"users.consent.agree": "Eu concordo!",
"users.consent.cookiesHeadline": "Cookies & Analytics",
"users.consent.cookiesText": "<p> Entendo e concordo que o Squidex usa cookies para garantir que você obtenha a melhor experiência em nossa plataforma e para armazenar seu status de login.. </p><p> Entendo e concordo que o Squidex integrou o Google Analytics (com a função de anonimizador). O Google Analytics é um serviço de análise da web para coletar e analisar dados sobre o comportamento dos utilizadors. </p><p> Eu aceito as <a href=\"{privacyUrl}\" target=\"_blank\" rel=\"noopener\">politicas privadas</a>.</p>",
"users.consent.emailHeadline": "E-Mailsa automáticos (opcional)",
"users.consent.emailText": "Entendo e concordo que o Squidex envia e-mails para me informar sobre novos ficheiros, alterações de última hora e tempos de inatividade.",
"users.consent.headline": "Nós precisamos do seu consentimento",
"users.consent.needed": "Voçê deve dar o seu consentimento.",
"users.consent.piiHeadline": "Informação pessoal",
"users.consent.piiText": "Entendo e concordo que o Squidex coleta as seguintes informações privadas que são recuperadas de provedores de autenticação externos, como Google, Microsoft ou Github. <ul class=\"personal-information\"> <li> Informações pessoais básicas (nome, sobrenome e foto) são fornecidas a todos os outros utilizadors para que possam adicioná-lo ao seu espaço de trabalho. </li><li> A qualquer momento, você tem a opção de alterar essas informações para anonimizar sua conta. </li><li> Sua conta de utilizador tem um identificador exclusivo e, para todas as suas alterações, rastreamos que você fez essas alterações e forneceu essas informações a outros utilizadors. </li></ul>",
"users.consent.title": "Consentimento",
"users.deleteYourselfError": "Não pode remover a si proprio.",
"users.error.headline": "Operação falhada",
"users.error.text": "Pedimos desculpa mas aconteceu algo de errado.",
"users.error.title": "Erro",
"users.errorHappened": "Ocorreu uma exceção inesperada.",
"users.lockedOutText": "Sua conta está bloqueada, entre em contato com o administrador.",
"users.lockedOutTitle": "Conta bloqueada",
"users.lockYourselfError": "Você não pode se trancar.",
"users.login.askAdmin": "",
"users.login.emailPlaceholder": "Introduzir Email",
"users.login.error": "Email ou password incorrecta",
"users.login.loginWith": "{action} com <strong>{provider}</strong>",
"users.login.noAccountLoginAction": "Carregue aqui para entrar",
"users.login.noAccountLoginQuestion": "Já registado?",
"users.login.noAccountSignupAction": "Carregue aqui para registar",
"users.login.noAccountSignupQuestion": "Sem conta?",
"users.login.passwordPlaceholder": "Introduzir Password",
"users.login.separator": "OU",
"users.logout.headline": "Desconectado!",
"users.logout.text": "!Por favor feche a janela.",
"users.logout.title": "Sair",
"users.noEmailAddress": "Não podemos obter o endereço de e-mail do provedor de autenticação.",
"users.profile.addLoginDone": "Login adicionado com sucesso.",
"users.profile.changePassword": "Alterar Password",
"users.profile.changePasswordDone": "Password alterada com sucesso.",
"users.profile.clientHint": "Use as credenciais do cliente para acessar a API com suas informações de perfil e permissões",
"users.profile.clientTitle": "cliente",
"users.profile.confirmPassword": "Confirmar",
"users.profile.generateClient": "Gerar",
"users.profile.generateClientDone": "Segredo do cliente gerado com sucesso.",
"users.profile.headline": "Editar perfil",
"users.profile.hideProfile": "Não mostre o seu perfil a outros",
"users.profile.loginsTitle": "Logins",
"users.profile.passwordTitle": "Password",
"users.profile.pii": "Informação pessoal",
"users.profile.propertiesHint": "Use propriedades personalizadas para regras e scripts.",
"users.profile.propertiesTitle": "Properiedades",
"users.profile.propertyAdd": "Adicionar propriedade",
"users.profile.removeLoginDone": "Provedor de login removido com sucesso.",
"users.profile.setPassword": "Set Password",
"users.profile.setPasswordDone": "Password configurada com sucesso.",
"users.profile.title": "Perfil",
"users.profile.updateProfileDone": "Conta atualizada com sucesso.",
"users.profile.updatePropertiesDone": "Conta atualizada com sucesso.",
"users.profile.uploadPicture": "Carregar foto",
"users.profile.uploadPictureDone": "Foto carregada com sucesso.",
"users.unlockYourselfError": "Você não pode desbloquear a si mesmo.",
"users.userLocked": "O usuário não tem permissão para fazer login.",
"users.userNotFound": "Não foi possível encontrar o utilizador.",
"validation.between": "{property|upper} deve estar entre {min} e {max}.",
"validation.greaterEqualsThan": "{property|upper} deve ser maior ou igual {other|lower}.",
"validation.greaterThan": "{property|upper} deve ser maior que {other|lower}.",
"validation.javascriptProperty": "{property|upper} não é um nome de propriedade Javascript.",
"validation.lessEqualsThan": "{property|upper} deve ser menor ou igual {other|lower}.",
"validation.lessThan": "{property|upper} deve ser menor que {other|lower}.",
"validation.notAnImage": "A imagem não é uma imagem válida.",
"validation.notAnValidSvg": "O SVG é malicioso e contém tags de script.",
"validation.onlyOneFile": "Só pode carregar um ficheiro.",
"validation.required": "{property|upper} é obrigatório.",
"validation.requiredBoth": "Se {property1|lower} ou {property2|lower} são utilizador ambos têm de ser definidos.",
"validation.requiredValue": "O valor deve ser definido.",
"validation.slug": "{property|upper} não é um slug válido.",
"validation.valid": "{property|upper} não é um valor válido.",
"workflows.overlap": "Múltiplos fluxos de trabalho cobrem todos os esquemas.",
"workflows.publishedIsInitial": "A etapa inicial não pode ser a etapa publicada.",
"workflows.publishedNotDefined": "O fluxo de trabalho deve ter uma etapa publicada.",
"workflows.publishedStepNotFound": "A transição tem um destino inválido.",
"workflows.schemaOverlap": "O esquema '{schema}' é coberto por vários fluxos de trabalho."
}

1111
backend/i18n/source/frontend_pt.json

File diff suppressed because it is too large

2
backend/i18n/translator/Squidex.Translator/Commands.cs

@ -140,7 +140,7 @@ public class Commands
throw new ArgumentException("Folder does not exist.", nameof(arguments));
}
var supportedLocales = new string[] { "en", "nl", "it", "zh" };
var supportedLocales = new string[] { "en", "nl", "it", "zh", "pt" };
var locales = supportedLocales;

13
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -89,6 +89,9 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
{
try
{
// We need to translate the query names to the document field names in MongoDB.
var query = q.Query.AdjustToModel(appId);
if (q.Ids is { Count: > 0 })
{
var filter = BuildFilter(appId, q.Ids.ToHashSet());
@ -98,10 +101,10 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
.SortByDescending(x => x.LastModified).ThenBy(x => x.Id)
.QueryLimit(q.Query)
.QuerySkip(q.Query)
.ToListRandomAsync(Collection, q.Query.Random, ct);
.ToListRandomAsync(Collection, query.Random, ct);
long assetTotal = assetEntities.Count;
if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0)
if (assetEntities.Count >= query.Take || query.Skip > 0)
{
if (q.NoTotal)
{
@ -117,8 +120,6 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
}
else
{
var query = q.Query.AdjustToModel(appId);
// Default means that no other filters are applied and we only query by app.
var (filter, isDefault) = query.BuildFilter(appId, parentId);
@ -130,9 +131,9 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
.ToListRandomAsync(Collection, query.Random, ct);
long assetTotal = assetEntities.Count;
if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0)
if (assetEntities.Count >= query.Take || query.Skip > 0)
{
var isDefaultQuery = q.Query.Filter == null;
var isDefaultQuery = query.Filter == null;
if (q.NoTotal || (q.NoSlowTotal && !isDefaultQuery))
{

14
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -180,17 +180,17 @@ public sealed class MongoContentCollection : MongoRepositoryBase<MongoContentEnt
return await queryScheduled.QueryAsync(app, new List<ISchemaEntity> { schema }, q, ct);
}
if (q.Referencing == default)
if (q.Referencing != default)
{
if (queryInDedicatedCollection != null)
{
return await queryInDedicatedCollection.QueryAsync(schema, q, ct);
}
return await queryReferences.QueryAsync(app, new List<ISchemaEntity> { schema }, q, ct);
}
return await queryByQuery.QueryAsync(schema, q, ct);
if (queryInDedicatedCollection != null)
{
return await queryInDedicatedCollection.QueryAsync(schema, q, ct);
}
return ResultList.Empty<IContentEntity>();
return await queryByQuery.QueryAsync(schema, q, ct);
}
catch (MongoCommandException ex) when (ex.Code == 96)
{

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -106,8 +106,8 @@ public static class Extensions
var result =
collection.Find(filter)
.QuerySort(query)
.QueryLimit(query)
.QuerySkip(query)
.QueryLimit(query)
.ToListRandomAsync(collection, query.Random, ct);
return await result;

24
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs

@ -28,7 +28,8 @@ internal sealed class QueryByIds : OperationBase
return ReadonlyList.Empty<ContentIdStatus>();
}
var filter = CreateFilter(appId, null, ids);
// Create a filter from the Ids and ensure that the content ids match to the app ID.
var filter = CreateFilter(appId, null, ids, null);
var contentEntities = await Collection.FindStatusAsync(filter, ct);
@ -43,12 +44,16 @@ internal sealed class QueryByIds : OperationBase
return ResultList.Empty<IContentEntity>();
}
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet());
// We need to translate the query names to the document field names in MongoDB.
var query = q.Query.AdjustToModel(app.Id);
var contentEntities = await FindContentsAsync(q.Query, filter, ct);
// Create a filter from the Ids and ensure that the content ids match to the schema IDs.
var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet(), query.Filter);
var contentEntities = await FindContentsAsync(query, filter, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= q.Query.Take || q.Query.Skip > 0)
if (contentTotal >= query.Take || query.Skip > 0)
{
if (q.NoTotal)
{
@ -68,14 +73,16 @@ internal sealed class QueryByIds : OperationBase
{
var result =
Collection.Find(filter)
.QueryLimit(query)
.QuerySort(query)
.QuerySkip(query)
.QueryLimit(query)
.ToListRandomAsync(Collection, query.Random, ct);
return await result;
}
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId>? schemaIds, HashSet<DomainId> ids)
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId>? schemaIds, HashSet<DomainId> ids,
FilterNode<ClrValue>? filter)
{
var filters = new List<FilterDefinition<MongoContentEntity>>();
@ -101,6 +108,11 @@ internal sealed class QueryByIds : OperationBase
filters.Add(Filter.Ne(x => x.IsDeleted, true));
if (filter != null)
{
filters.Add(filter.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
}

8
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -66,9 +66,9 @@ internal sealed class QueryByQuery : OperationBase
var contentEntities = await Collection.QueryContentsAsync(filter, query, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= q.Query.Take || q.Query.Skip > 0)
if (contentTotal >= query.Take || query.Skip > 0)
{
if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null))
if (q.NoTotal || (q.NoSlowTotal && query.Filter != null))
{
contentTotal = -1;
}
@ -98,9 +98,9 @@ internal sealed class QueryByQuery : OperationBase
var contentEntities = await Collection.QueryContentsAsync(filter, query, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= q.Query.Take || q.Query.Skip > 0)
if (contentTotal >= query.Take || query.Skip > 0)
{
if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null))
if (q.NoTotal || (q.NoSlowTotal && query.Filter != null))
{
contentTotal = -1;
}

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs

@ -91,9 +91,9 @@ internal sealed class QueryInDedicatedCollection : MongoBase<MongoContentEntity>
var contentEntities = await contentCollection.QueryContentsAsync(filter, query, ct);
var contentTotal = (long)contentEntities.Count;
if (contentTotal >= q.Query.Take || q.Query.Skip > 0)
if (contentTotal >= query.Take || query.Skip > 0)
{
if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null))
if (q.NoTotal || (q.NoSlowTotal && query.Filter != null))
{
contentTotal = -1;
}

1276
backend/src/Squidex.Shared/Texts.pt.resx

File diff suppressed because it is too large

2
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -43,8 +43,6 @@ public sealed class ContentsSharedController : ApiController
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous]

2
backend/src/Squidex/Config/Web/WebExtensions.cs

@ -32,7 +32,7 @@ public static class WebExtensions
public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app)
{
var supportedCultures = new[] { "en", "nl", "it", "zh" };
var supportedCultures = new[] { "en", "nl", "it", "zh", "pt" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])

51
backend/src/Squidex/wwwroot/scripts/editor-sdk.js

@ -39,6 +39,20 @@ function isFunction(value) {
return typeof value === 'function';
}
function isArrayOfStrings(value) {
if (!Array.isArray(value)) {
return false;
}
for (var i = 0; i < value.length; i++) {
if (!isString(value[i])) {
return false;
}
}
return true;
}
function SquidexPlugin() {
var initHandler;
var initCalled = false;
@ -82,7 +96,7 @@ function SquidexPlugin() {
var editor = {
/**
* Get the current value.
* Get the current context.
*/
getContext: function () {
return context;
@ -140,13 +154,13 @@ function SquidexPlugin() {
};
return editor;
}
function SquidexFormField() {
var context;
var currentConfirm;
var currentPickAssets;
var currentPickContents;
var disabled = false;
var disabledHandler;
var formValue;
@ -218,6 +232,8 @@ function SquidexFormField() {
if (event.source !== window) {
var type = event.data.type;
console.log('Received Message: ' + type);
if (type === 'disabled') {
var newDisabled = event.data.isDisabled;
@ -278,6 +294,14 @@ function SquidexFormField() {
currentPickAssets.callback(event.data.result);
}
}
} else if (type === 'pickContentsResult') {
var correlationId = event.data.correlationId;
if (currentPickContents && currentPickContents.correlationId === correlationId) {
if (typeof currentPickContents.callback === 'function') {
currentPickContents.callback(event.data.result);
}
}
}
}
}
@ -462,6 +486,29 @@ function SquidexFormField() {
}
},
/**
* Shows the dialog to pick assets.
*
* @param {string} schemas: The list of schema names.
* @param {function} callback The callback to invoke when the dialog is completed or closed.
*/
pickContents: function (schemas, callback) {
if (!isFunction(callback) || !isArrayOfStrings(schemas)) {
return;
}
var correlationId = new Date().getTime().toString();
currentPickContents = {
correlationId: correlationId,
callback: callback
};
if (window.parent) {
window.parent.postMessage({ type: 'pickContents', correlationId: correlationId, schemas: schemas }, '*');
}
},
/**
* Register an function that is called when the field is initialized.
*

161
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -6,11 +6,9 @@
// ==========================================================================
using System.Net;
using System.Runtime.Intrinsics.X86;
using Squidex.Assets;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using Xunit.Sdk;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
@ -80,44 +78,12 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
var numUploads = 0;
var numConflicts = 0;
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (progress.Asset == null)
{
// When the previous request is still in progress we just give it another try.
if (progress.Exception is SquidexManagementException { StatusCode: 409 } && numConflicts < 3)
{
numConflicts++;
progress.ResetException();
// Wait a little bit to finish the request on the server.
await Task.Delay(100, cts.Token);
}
else if (progress.Exception != null)
{
break;
}
pausingStream.Reset();
await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(), cts.Token);
numUploads++;
}
}
await UploadInChunksAsync(fileParameter);
Assert.Null(progress.Exception);
Assert.NotEmpty(progress.Progress);
Assert.NotNull(progress.Asset);
Assert.True(numUploads > 1);
Assert.True(progress.Uploads.Count > 1);
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
@ -242,7 +208,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
[Fact]
public async Task Should_replace_asset_using_tus_in_chunks()
{
for (var i = 0; i < 5; i++)
for (var i = 0; i < 1; i++)
{
// STEP 1: Create asset
var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png");
@ -253,45 +219,12 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
var numUploads = 0;
var numConflicts = 0;
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
// When the previous request is still in progress we just give it another try.
while (progress.Asset == null)
{
// When the previous request is still in progress we just give it another try.
if (progress.Exception is SquidexManagementException { StatusCode: 409 } && numConflicts < 3)
{
numConflicts++;
progress.ResetException();
// Wait a little bit to finish the request on the server.
await Task.Delay(100, cts.Token);
}
else if (progress.Exception != null)
{
break;
}
pausingStream.Reset();
await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(asset_1.Id), cts.Token);
numUploads++;
}
}
await UploadInChunksAsync(fileParameter, asset_1.Id);
Assert.Null(progress.Exception);
Assert.NotEmpty(progress.Progress);
Assert.NotNull(progress.Asset);
Assert.True(numUploads > 1);
Assert.True(progress.Uploads.Count > 1);
await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
{
@ -699,12 +632,36 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Assert.NotEqual(asset_1.FileSize, asset_2.FileSize);
}
private async Task UploadInChunksAsync(FileParameter fileParameter, string id = null)
{
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType)
{
ContentLength = fileParameter.Data.Length
};
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (progress.Asset == null && progress.Exception == null && !cts.IsCancellationRequested)
{
pausingStream.Reset();
await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(id), cts.Token);
progress.Uploaded();
}
}
}
public class ProgressHandler : IAssetProgressHandler
{
public string FileId { get; private set; }
public string FileId { get; private set; } = Guid.NewGuid().ToString();
public List<int> Progress { get; } = new List<int>();
public List<int> Uploads { get; } = new List<int>();
public Exception Exception { get; private set; }
public AssetDto Asset { get; private set; }
@ -719,17 +676,14 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
return options;
}
public void ResetException()
public void Uploaded()
{
Exception = null;
Uploads.Add(Progress.LastOrDefault());
}
public Task OnCompletedAsync(AssetUploadCompletedEvent @event,
CancellationToken ct)
{
// This is a previous exception, so we can unset it.
ResetException();
Asset = @event.Asset;
return Task.CompletedTask;
}
@ -751,28 +705,44 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
public Task OnFailedAsync(AssetUploadExceptionEvent @event,
CancellationToken ct)
{
if (@event.Exception.InnerException is not PauseException)
{
Exception = @event.Exception;
}
Exception = @event.Exception;
return Task.CompletedTask;
}
}
private sealed class PauseException : Exception
public class PauseStream : DelegateStream
{
}
private readonly int maxLength;
private long totalRead;
private long totalRemaining;
private long seekStart;
private sealed class PauseStream : DelegateStream
{
private readonly double pauseAfter = 1;
private int totalRead;
public override long Length
{
get => Math.Min(maxLength, totalRemaining);
}
public override long Position
{
get => base.Position - seekStart;
set => throw new NotSupportedException();
}
public PauseStream(Stream innerStream, double pauseAfter)
: base(innerStream)
{
this.pauseAfter = pauseAfter;
maxLength = (int)Math.Floor(innerStream.Length * pauseAfter) + 1;
totalRemaining = innerStream.Length;
}
public override long Seek(long offset, SeekOrigin origin)
{
var position = seekStart = base.Seek(offset, origin);
totalRemaining = base.Length - position;
return position;
}
public void Reset()
@ -783,9 +753,16 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
if (totalRead > Length * pauseAfter)
var remaining = Length - totalRead;
if (remaining <= 0)
{
return 0;
}
if (remaining < buffer.Length)
{
throw new PauseException();
buffer = buffer[.. (int)remaining];
}
var bytesRead = await base.ReadAsync(buffer, cancellationToken);

2
backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs

@ -9,7 +9,7 @@ using TestSuite.Fixtures;
namespace TestSuite.ApiTests;
public sealed class ContentFixture : TestSchemaFixtureBase
public class ContentFixture : TestSchemaFixtureBase
{
public ContentFixture()
: base("my-writes")

5
backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs

@ -38,7 +38,10 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase
nested2 = index
}
}),
Geo = GeoJson.Point(index, index, oldFormat: index % 2 == 1),
Geo = GeoJson.Point(
index + 100,
index,
oldFormat: index % 2 == 1),
Localized = new Dictionary<string, string>
{
["en"] = index.ToString(CultureInfo.InvariantCulture)

8
backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -361,7 +361,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
[Fact]
public async Task Should_query_by_near_location_with_odata()
{
var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" };
var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(103 3)') lt 1000" };
var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30));
@ -381,7 +381,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
op = "lt",
value = new
{
longitude = 3,
longitude = 103,
latitude = 3,
distance = 1000
}
@ -397,7 +397,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
[Fact]
public async Task Should_query_by_near_geoson_location_with_odata()
{
var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" };
var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(104 4)') lt 1000" };
var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30));
@ -466,7 +466,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
op = "lt",
value = new
{
longitude = 4,
longitude = 104,
latitude = 4,
distance = 1000
}

195
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs

@ -0,0 +1,195 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests;
public sealed class GraphQLFixture : ContentFixture
{
public sealed class DynamicEntity : Content<object>
{
}
public override async Task InitializeAsync()
{
await base.InitializeAsync();
await CreateSchemasAsync();
await CreateContentsAsync();
}
private async Task CreateSchemasAsync()
{
async Task<string> CreateSchemaAsync(CreateSchemaDto request)
{
try
{
var response = await Schemas.PostSchemaAsync(AppName, request);
return response.Id;
}
catch (SquidexManagementException ex)
{
if (ex.StatusCode != 400)
{
throw;
}
var schema = await Schemas.GetSchemaAsync(AppName, request.Name);
return schema.Id;
}
}
// STEP 1: Create cities schema.
var createCitiesRequest = new CreateSchemaDto
{
Name = "cities",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
}
},
IsPublished = true
};
var citiesId = await CreateSchemaAsync(createCitiesRequest);
// STEP 2: Create states schema.
var createStatesRequest = new CreateSchemaDto
{
Name = "states",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
},
new UpsertSchemaFieldDto
{
Name = "cities",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<string> { citiesId }
}
}
},
IsPublished = true
};
var statesId = await CreateSchemaAsync(createStatesRequest);
// STEP 3: Create countries schema.
var createCountriesRequest = new CreateSchemaDto
{
Name = "countries",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
},
new UpsertSchemaFieldDto
{
Name = "states",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<string> { statesId }
}
}
},
IsPublished = true
};
await CreateSchemaAsync(createCountriesRequest);
}
private async Task CreateContentsAsync()
{
var countriesClient = ClientManager.CreateContentsClient<DynamicEntity, object>("countries");
var countriesResponse = await countriesClient.GetAsync();
if (countriesResponse.Total > 0)
{
return;
}
async Task<string> CreateCityAsync(string name)
{
var citySAData = new
{
name = new
{
iv = name
}
};
var citiesClient = ClientManager.CreateContentsClient<DynamicEntity, object>("cities");
var city = await citiesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish);
return city.Id;
}
async Task<string> CreateStateAsync(string name, string cityId)
{
var citySAData = new
{
name = new
{
iv = name
},
cities = new
{
iv = new[] { cityId }
}
};
var statesClient = ClientManager.CreateContentsClient<DynamicEntity, object>("states");
var state = await statesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish);
return state.Id;
}
// STEP 1: Create state 1
var sachsenCapital = await CreateCityAsync("Leipzig");
var sachstenState = await CreateStateAsync("Sachsen", sachsenCapital);
// STEP 1: Create state 2
var badenWCapital = await CreateCityAsync("Stuttgart");
var badenWState = await CreateStateAsync("Baden Württemberg", badenWCapital);
// STEP 3: Create country
var countryData = new
{
name = new
{
iv = "Germany"
},
states = new
{
iv = new[] { sachstenState, badenWState }
}
};
await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish);
}
}

144
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs

@ -0,0 +1,144 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Reactive.Linq;
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
using TestSuite.Model;
namespace TestSuite.ApiTests;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
public class GraphQLSubscriptionTests : IClassFixture<ContentFixture>
{
public ContentFixture _ { get; set; }
public GraphQLSubscriptionTests(ContentFixture fixture)
{
_ = fixture;
}
private sealed class ContentChangesResult
{
public ContentChanges ContentChanges { get; set; }
}
private sealed class ContentChanges
{
public string Id { get; set; }
}
private sealed class AssetChangesResult
{
public AssetChanges AssetChanges { get; set; }
}
private sealed class AssetChanges
{
public string Id { get; set; }
}
[Fact]
public async Task Should_listen_to_content_changes()
{
var client = await CreateClient();
// STEP 1: Subscribe to changes.
var contentChanges = new GraphQLRequest
{
Query = @"
subscription {
contentChanges {
id
}
}"
};
var contentId = Guid.NewGuid().ToString();
var subscriptionStream
= client.CreateSubscriptionStream<ContentChangesResult>(contentChanges);
var publishedContent =
subscriptionStream.Where(x => x.Data.ContentChanges.Id == contentId).Timeout(TimeSpan.FromSeconds(30))
.FirstOrDefaultAsync();
// STEP 2: Create Content.
await _.Contents.CreateAsync(new TestEntityData(), new ContentCreateOptions { Id = contentId });
// STEP 3: Wait for publication.
var publishedResult = await publishedContent;
Assert.Equal(contentId, publishedResult.Data.ContentChanges.Id);
}
[Fact]
public async Task Should_listen_to_asset_changes()
{
var client = await CreateClient();
// STEP 1: Subscribe to changes.
var assetChanges = new GraphQLRequest
{
Query = @"
subscription {
assetChanges {
id
}
}"
};
var assetId = Guid.NewGuid().ToString();
var subscriptionStream
= client.CreateSubscriptionStream<AssetChangesResult>(assetChanges);
var publishedAsset =
subscriptionStream.Where(x => x.Data.AssetChanges.Id == assetId).Timeout(TimeSpan.FromSeconds(30))
.FirstOrDefaultAsync();
// STEP 2: Create asset.
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
await using (fileParameter.Data)
{
await _.Assets.UploadAssetAsync(_.AppName, fileParameter, new AssetUploadOptions { Id = assetId });
}
// STEP 3: Wait for publication.
var publishedResult = await publishedAsset;
Assert.Equal(assetId, publishedResult.Data.AssetChanges.Id);
}
private async Task<GraphQLHttpClient> CreateClient()
{
var accessToken = await _.ClientManager.Options.Authenticator.GetBearerTokenAsync(_.AppName, default);
var options = new GraphQLHttpClientOptions
{
EndPoint = new Uri(_.ClientManager.GenerateUrl($"/api/content/{_.AppName}/graphql?access_token={accessToken}"))
};
var client = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer());
await client.InitializeWebsocketConnection();
return client;
}
}

277
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -7,7 +7,6 @@
using Newtonsoft.Json.Linq;
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
using TestSuite.Model;
#pragma warning disable SA1300 // Element should begin with upper-case letter
@ -15,53 +14,15 @@ using TestSuite.Model;
namespace TestSuite.ApiTests;
public sealed class GraphQLTests : IClassFixture<ContentFixture>
public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
{
public ContentFixture _ { get; }
public GraphQLFixture _ { get; }
public GraphQLTests(ContentFixture fixture)
public GraphQLTests(GraphQLFixture fixture)
{
_ = fixture;
}
public sealed class DynamicEntity : Content<object>
{
}
public sealed class Country
{
public CountryData Data { get; set; }
}
public sealed class CountryData
{
public string Name { get; set; }
public List<State> States { get; set; }
}
public sealed class State
{
public StateData Data { get; set; }
}
public sealed class StateData
{
public string Name { get; set; }
public List<City> Cities { get; set; }
}
public sealed class City
{
public CityData Data { get; set; }
}
public sealed class CityData
{
public string Name { get; set; }
}
[Fact]
public async Task Should_query_json()
{
@ -92,38 +53,20 @@ public sealed class GraphQLTests : IClassFixture<ContentFixture>
}".Replace("<ID>", content_0.Id, StringComparison.Ordinal)
};
var result1 = await _.SharedContents.GraphQlAsync<JToken>(query);
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
Assert.Equal(1, result1["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>());
Assert.Equal(2, result1["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>());
Assert.Equal(1, result["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>());
Assert.Equal(2, result["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>());
}
[Fact]
public async Task Should_create_and_query_with_graphql()
public async Task Should_query_graphql_reference_selectors()
{
try
{
await CreateSchemasAsync();
}
catch
{
// Do nothing
}
try
{
await CreateContentsAsync();
}
catch
{
// Do nothing
}
var query = new
{
query = @"
{
queryCountriesContents {
countries: queryCountriesContents {
data: flatData {
name,
states {
@ -141,134 +84,144 @@ public sealed class GraphQLTests : IClassFixture<ContentFixture>
}"
};
var result1 = await _.SharedContents.GraphQlAsync<JToken>(query);
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
var typed = result1["queryCountriesContents"].ToObject<List<Country>>();
var cityNames =
result["countries"].ToObject<List<Country>>()[0].Data.States
.SelectMany(x => x.Data.Cities)
.Select(x => x.Data.Name)
.Order();
Assert.Equal("Leipzig", typed[0].Data.States[0].Data.Cities[0].Data.Name);
Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames);
}
private async Task CreateSchemasAsync()
[Fact]
public async Task Should_query_graphql_reference_operator()
{
// STEP 1: Create cities schema.
var createCitiesRequest = new CreateSchemaDto
var query = new
{
Name = "cities",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
query = @"
{
Name = "name",
Properties = new StringFieldPropertiesDto()
}
},
IsPublished = true
countries: queryCountriesContents {
data: flatData {
name,
states {
data: flatData {
name
},
cities: referencesCitiesContents {
data: flatData {
name
}
}
}
}
}
}"
};
var cities = await _.Schemas.PostSchemaAsync(_.AppName, createCitiesRequest);
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
var cityNames =
result["countries"]
.SelectMany(x => x["data"]["states"])
.SelectMany(x => x["cities"])
.Select(x => x["data"]["name"].Value<string>())
.Order();
Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames);
}
// STEP 2: Create states schema.
var createStatesRequest = new CreateSchemaDto
[Fact]
public async Task Should_query_graphql_reference_operator_with_filter()
{
var query = new
{
Name = "states",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
},
new UpsertSchemaFieldDto
query = @"
{
Name = "cities",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<string> { cities.Id }
countries: queryCountriesContents {
data: flatData {
name,
states {
data: flatData {
name
},
cities: referencesCitiesContents(filter: ""data/name/iv eq 'Leipzig'"") {
data: flatData {
name
}
}
}
}
}
}
},
IsPublished = true
}"
};
var states = await _.Schemas.PostSchemaAsync(_.AppName, createStatesRequest);
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
// STEP 3: Create countries schema.
var createCountriesRequest = new CreateSchemaDto
{
Name = "countries",
Fields = new List<UpsertSchemaFieldDto>
{
new UpsertSchemaFieldDto
{
Name = "name",
Properties = new StringFieldPropertiesDto()
},
new UpsertSchemaFieldDto
{
Name = "states",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<string> { states.Id }
}
}
},
IsPublished = true
};
var cityNames =
result["countries"]
.SelectMany(x => x["data"]["states"])
.SelectMany(x => x["cities"])
.Select(x => x["data"]["name"].Value<string>())
.Order();
await _.Schemas.PostSchemaAsync(_.AppName, createCountriesRequest);
Assert.Equal(new[] { "Leipzig" }, cityNames);
}
private async Task CreateContentsAsync()
[Fact]
public async Task Should_query_graphql_referencing_operator()
{
// STEP 1: Create city
var cityData = new
var query = new
{
name = new
{
iv = "Leipzig"
}
query = @"
{
cities: queryCitiesContents {
states: referencingStatesContents {
data: flatData {
name
}
}
}
}"
};
var citiesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("cities");
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
var city = await citiesClient.CreateAsync(cityData, ContentCreateOptions.AsPublish);
var stateNames =
result["cities"]
.SelectMany(x => x["states"])
.Select(x => x["data"]["name"].Value<string>())
.Order();
Assert.Equal(new[] { "Baden Württemberg", "Sachsen" }, stateNames);
}
// STEP 2: Create city
var stateData = new
[Fact]
public async Task Should_query_graphql_referencing_operator_with_filter()
{
var query = new
{
name = new
{
iv = "Saxony"
},
cities = new
{
iv = new[] { city.Id }
}
query = @"
{
cities: queryCitiesContents {
states: referencingStatesContents(filter: ""data/name/iv eq 'Sachsen'"") {
data: flatData {
name
}
}
}
}"
};
var statesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("states");
var state = await statesClient.CreateAsync(stateData, ContentCreateOptions.AsPublish);
// STEP 3: Create country
var countryData = new
{
name = new
{
iv = "Germany"
},
states = new
{
iv = new[] { state.Id }
}
};
var result = await _.SharedContents.GraphQlAsync<JToken>(query);
var countriesClient = _.ClientManager.CreateContentsClient<DynamicEntity, object>("countries");
var stateNames =
result["cities"]
.SelectMany(x => x["states"])
.Select(x => x["data"]["name"].Value<string>())
.Order();
await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish);
Assert.Equal(new[] { "Sachsen" }, stateNames);
}
}

12
backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -15,14 +15,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Codeuctivity.ImageSharpCompare" Version="2.0.76" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.704">
<PackageReference Include="GraphQL.Client" Version="5.1.0" />
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="5.1.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.750">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NSwag.Core" Version="13.16.1" />
<PackageReference Include="PuppeteerSharp" Version="7.1.0" />
<PackageReference Include="Squidex.Assets" Version="4.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="NSwag.Core" Version="13.18.0" />
<PackageReference Include="PuppeteerSharp" Version="8.0.0" />
<PackageReference Include="Squidex.Assets" Version="5.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify.Xunit" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />

4
backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj

@ -6,11 +6,11 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.704">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.750">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

44
backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable MA0048 // File name must match type name
namespace TestSuite.Model;
public sealed class Country
{
public CountryData Data { get; set; }
}
public sealed class CountryData
{
public string Name { get; set; }
public List<State> States { get; set; }
}
public sealed class State
{
public StateData Data { get; set; }
}
public sealed class StateData
{
public string Name { get; set; }
public List<City> Cities { get; set; }
}
public sealed class City
{
public CityData Data { get; set; }
}
public sealed class CityData
{
public string Name { get; set; }
}

16
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -6,18 +6,18 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.704">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.750">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="12.3.0" />
<PackageReference Include="Squidex.ClientLibrary.ServiceExtensions" Version="12.3.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="12.5.0" />
<PackageReference Include="Squidex.ClientLibrary.ServiceExtensions" Version="12.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />

33
frontend/package-lock.json

@ -28,7 +28,7 @@
"angular-mentions": "1.5.0",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
"bootstrap": "5.2.2",
"bootstrap": "5.2.3",
"core-js": "3.25.5",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "2.29.3",
@ -14773,7 +14773,9 @@
"license": "ISC"
},
"node_modules/bootstrap": {
"version": "5.2.2",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz",
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"funding": [
{
"type": "github",
@ -14784,7 +14786,6 @@
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.6"
}
@ -16252,8 +16253,9 @@
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
@ -24457,7 +24459,8 @@
},
"node_modules/object-assign": {
"version": "4.1.1",
"license": "MIT",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
@ -26621,8 +26624,9 @@
},
"node_modules/proxy-middleware": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz",
"integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
@ -28248,8 +28252,9 @@
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@ -42433,7 +42438,9 @@
"dev": true
},
"bootstrap": {
"version": "5.2.2",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz",
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"requires": {}
},
"boxen": {
@ -43406,6 +43413,8 @@
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"requires": {
"object-assign": "^4",
@ -48838,7 +48847,9 @@
"dev": true
},
"object-assign": {
"version": "4.1.1"
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-copy": {
"version": "0.1.0",
@ -50125,6 +50136,8 @@
},
"proxy-middleware": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz",
"integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==",
"dev": true
},
"prr": {
@ -51183,6 +51196,8 @@
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dev": true,
"requires": {
"debug": "2.6.9",

2
frontend/package.json

@ -35,7 +35,7 @@
"angular-mentions": "1.5.0",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
"bootstrap": "5.2.2",
"bootstrap": "5.2.3",
"core-js": "3.25.5",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "2.29.3",

22
frontend/src/app/_theme.html

@ -100,7 +100,7 @@
</ul>
<form class="d-flex">
<input class="form-control me-2" type="text" placeholder="Search">
<input class="form-control me-2" placeholder="Search">
<button type="button" class="btn btn-secondary">Search</button>
</form>
@ -131,7 +131,7 @@
</ul>
<form class="d-flex">
<input class="form-control me-2" type="text" placeholder="Search">
<input class="form-control me-2" placeholder="Search">
<button type="button" class="btn btn-secondary">Search</button>
</form>
@ -676,19 +676,19 @@
<div class="form-group">
<label>Disabled input</label>
<input class="form-control" type="text" placeholder="Disabled input here..." disabled>
<input class="form-control" placeholder="Disabled input here..." disabled>
</div>
<div class="form-group">
<label>Readonly input</label>
<input class="form-control" type="text" placeholder="Disabled input here..." readonly>
<input class="form-control" placeholder="Disabled input here..." readonly>
</div>
<div class="form-group has-success">
<label>Input with success</label>
<input type="text" class="form-control is-valid">
<input class="form-control is-valid">
<div class="valid-feedback">Success! You've done it.</div>
</div>
@ -696,7 +696,7 @@
<div class="form-group has-warning">
<label>Input with warning</label>
<input type="text" class="form-control is-invalid">
<input class="form-control is-invalid">
<div class="invalid-feedback">Shucks, try again.</div>
</div>
@ -708,13 +708,13 @@
<div class="errors">You have entered an invalid value.</div>
</div>
<input type="text" class="form-control is-invalid">
<input class="form-control is-invalid">
</div>
<div class="form-group has-warning">
<label>Input with hint</label>
<input type="text" class="form-control">
<input class="form-control">
<div class="alert alert-hint mt-2">
<i class="icon-info-outline"></i> The app name cannot be changed later.
@ -730,7 +730,7 @@
<div class="form-group">
<label>Default input</label>
<input type="text" class="form-control">
<input class="form-control">
</div>
<div class="form-group">
@ -745,7 +745,7 @@
<div class="input-group mb-3">
<span class="input-group-text">$</span>
<input type="text" class="form-control"
<input class="form-control"
aria-label="Amount (to the nearest dollar)">
<span class="input-group-text">.00</span>
@ -758,7 +758,7 @@
<div class="input-group mb-3">
<button class="btn btn-outline-secondary">Button</button>
<input type="text" class="form-control"
<input class="form-control"
aria-label="Amount (to the nearest dollar)">
<button class="btn btn-outline-secondary">Button</button>

2
frontend/src/app/features/administration/pages/users/user-page.component.html

@ -46,7 +46,7 @@
<sqx-control-errors for="displayName"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="off" spellcheck="false">
<input class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="off" spellcheck="false">
</div>
<div class="form-group form-group-section">

2
frontend/src/app/features/assets/pages/asset-tag-dialog.component.html

@ -12,7 +12,7 @@
<sqx-control-errors for="tagName"></sqx-control-errors>
<input type="text" class="form-control" id="tagName" formControlName="tagName" autocomplete="off" sqxFocusOnInit>
<input class="form-control" id="tagName" formControlName="tagName" autocomplete="off" sqxFocusOnInit>
</div>
</ng-container>

2
frontend/src/app/features/content/shared/forms/assets-editor.component.html

@ -80,5 +80,5 @@
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-assets-selector (select)="selectAssets($event)"></sqx-assets-selector>
<sqx-asset-selector (select)="selectAssets($event)"></sqx-asset-selector>
</ng-container>

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

@ -28,13 +28,14 @@
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" #editor
[context]="formContext"
[expanded]="isExpanded"
(expandedChange)="toggleExpanded()"
[isExpanded]="isExpanded"
(isExpandedChange)="toggleExpanded()"
[formControlBinding]="$any(fieldForm)"
[formValue]="form.valueChanges | async"
[formIndex]="index"
[formField]="formModel.field.name"
[language]="language.iso2Code">
[language]="language"
[languages]="languages">
</sqx-iframe-editor>
</ng-container>
@ -173,10 +174,10 @@
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
<input class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
<input class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" rows="5"></textarea>

17
frontend/src/app/features/content/shared/forms/iframe-editor.component.html

@ -1,11 +1,20 @@
<div #container>
<div #inner [class.fullscreen]="snapshot.isFullscreen">
<iframe #iframe scrolling="no" width="100%" [style.height]="0" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>
<div #inner [class.fullscreen]="snapshot.isFullscreen" [class.expanded]="isExpanded">
<iframe #iframe [scrolling]="!isExpanded ? 'no' : 'yes'" width="100%" [style.height]="0" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>
</div>
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-assets-selector
<sqx-asset-selector
(select)="pickAssets($event)">
</sqx-assets-selector>
</sqx-asset-selector>
</ng-container>
<ng-container *sqxModal="contentsDialog">
<sqx-content-selector
(select)="pickContents($event)"
[language]="language"
[languages]="languages"
[schemaNames]="contentsSchemas">
</sqx-content-selector>
</ng-container>

11
frontend/src/app/features/content/shared/forms/iframe-editor.component.scss

@ -17,4 +17,15 @@ iframe {
iframe {
height: 100% !important;
}
}
.expanded {
@include absolute(50px, 0, 0, 0);
overflow: hidden;
iframe {
height: 100% !important;
overflow-x: auto !important;
overflow-y: auto !important;
}
}

43
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Even
import { AbstractControl } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogModel, DialogService, disabled$, StatefulComponent, Types, value$ } from '@app/framework';
import { AppsState, AssetDto, computeEditorUrl } from '@app/shared';
import { AppLanguageDto, AppsState, AssetDto, computeEditorUrl, ContentDto } from '@app/shared';
interface State {
// True, when the editor is shown as fullscreen.
@ -17,7 +17,7 @@ interface State {
}
@Component({
selector: 'sqx-iframe-editor[context][formField][formIndex][formValue][formControlBinding]',
selector: 'sqx-iframe-editor[context][formField][formIndex][formValue][formControlBinding][language][languages]',
styleUrls: ['./iframe-editor.component.scss'],
templateUrl: './iframe-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -26,7 +26,6 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
private value: any;
private isInitialized = false;
private isDisabled = false;
private assetsCorrelationId: any;
@ViewChild('iframe', { static: false })
public iframe!: ElementRef<HTMLIFrameElement>;
@ -38,10 +37,10 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
public inner!: ElementRef<HTMLElement>;
@Output()
public expandedChange = new EventEmitter();
public isExpandedChange = new EventEmitter();
@Input()
public expanded = false;
public isExpanded = false;
@Input()
public context: any = {};
@ -56,7 +55,10 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
public formIndex?: number | null;
@Input()
public language?: string | null;
public language!: AppLanguageDto;
@Input()
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
public formControlBinding!: AbstractControl;
@ -73,8 +75,13 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
public computedUrl = '';
public assetsCorrelationId: any;
public assetsDialog = new DialogModel();
public contentsCorrelationId: any;
public contentsSchemas?: string[];
public contentsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -161,8 +168,8 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
} else if (type === 'expanded') {
const { mode } = event.data;
if (mode !== this.expanded) {
this.expandedChange.emit();
if (mode !== this.isExpanded) {
this.isExpandedChange.emit();
}
} else if (type === 'valueChanged') {
const { value } = event.data;
@ -201,6 +208,14 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
this.assetsCorrelationId = correlationId;
this.assetsDialog.show();
}
} else if (type === 'pickContents') {
const { correlationId, schemas } = event.data;
if (correlationId) {
this.contentsCorrelationId = correlationId;
this.contentsSchemas = schemas;
this.contentsDialog.show();
}
}
this.detectChanges();
@ -217,6 +232,16 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
this.assetsDialog.hide();
}
public pickContents(contents: ReadonlyArray<ContentDto>) {
if (this.contentsCorrelationId) {
this.sendMessage('pickContentsResult', { correlationId: this.contentsCorrelationId, result: contents });
this.contentsCorrelationId = null;
}
this.contentsDialog.hide();
}
public updateValue(obj: any) {
if (!Types.equals(obj, this.value)) {
this.value = obj;
@ -250,7 +275,7 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
}
private sendExpanded() {
this.sendMessage('expandedChanged', { expanded: this.expanded });
this.sendMessage('expandedChanged', { expanded: this.isExpanded });
}
private sendDisabled() {

2
frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html

@ -8,7 +8,7 @@
<i class="icon-search"></i>
</button>
<input class="form-control" [formControl]="valueControl" readonly>
<input readonly [disabled]="true" class="form-control" [formControl]="valueControl">
</div>
<div *ngIf="stockPhotoThumbnail | async; let url;" class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'">

2
frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.html

@ -21,7 +21,7 @@
<span class="truncate">{{triggerSchema.schema.displayName}}</span>
</td>
<td class="text-center">
<input type="text" class="form-control code" placeholder="{{ 'rules.conditionHint' | sqxTranslate }}"
<input class="form-control code" placeholder="{{ 'rules.conditionHint' | sqxTranslate }}"
[disabled]="triggerForm.form.disabled"
[ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition"

6
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -7,7 +7,7 @@
<div class="form-group">
<label for="name">{{ 'common.name' | sqxTranslate }}</label>
<input type="text" class="form-control" id="name" readonly [ngModel]="schema.name" [ngModelOptions]="{ standalone: true }">
<input readonly class="form-control" id="name" [ngModel]="schema.name" [ngModelOptions]="{ standalone: true }">
</div>
<div class="form-group">
@ -15,7 +15,7 @@
<sqx-control-errors for="label"></sqx-control-errors>
<input type="text" class="form-control" id="label" formControlName="label">
<input class="form-control" id="label" formControlName="label">
<sqx-form-hint>{{ 'schemas.schemaLabelHint' | sqxTranslate }}</sqx-form-hint>
</div>
@ -25,7 +25,7 @@
<sqx-control-errors for="hints"></sqx-control-errors>
<textarea type="text" class="form-control" id="hints" formControlName="hints" rows="4"></textarea>
<textarea class="form-control" id="hints" formControlName="hints" rows="4"></textarea>
<sqx-form-hint>{{ 'schemas.schemaHintsHint' | sqxTranslate }}</sqx-form-hint>
</div>

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

@ -53,7 +53,7 @@
<div class="form-group">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit>
<input class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit>
</div>
<div class="form-group" *ngIf="schema.type !== 'Component' && !parent && (addFieldForm.isContentField | async)">

6
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldName">{{ 'common.name' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{ standalone: true }">
<input readonly class="form-control" id="{{field.fieldId}}_fieldName" [ngModel]="field.name" [ngModelOptions]="{ standalone: true }">
<sqx-form-hint>
{{ 'schemas.field.nameHint' | sqxTranslate }}
@ -17,7 +17,7 @@
<div class="col-9">
<sqx-control-errors for="label"></sqx-control-errors>
<input type="text" class="form-control" id="{{field.fieldId}}_fieldLabel" maxlength="100" formControlName="label">
<input class="form-control" id="{{field.fieldId}}_fieldLabel" maxlength="100" formControlName="label">
<sqx-form-hint>
{{ 'schemas.field.labelHint' | sqxTranslate }}
@ -31,7 +31,7 @@
<div class="col-9">
<sqx-control-errors for="hints"></sqx-control-errors>
<input type="text" class="form-control" id="{{field.fieldId}}_fieldHints" maxlength="1000" formControlName="hints">
<input class="form-control" id="{{field.fieldId}}_fieldHints" maxlength="1000" formControlName="hints">
<sqx-form-hint>
{{ 'schemas.field.hintsHint' | sqxTranslate }}

2
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.html

@ -60,6 +60,6 @@
</sqx-field-form-ui>
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 3">
<div class="table-items-row-details-tab" *ngIf="selectedTab === 3">
<sqx-json-more [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-json-more>
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<input class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint>
{{ 'schemas.field.placeholderHint' | sqxTranslate }}

4
frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<input class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint>
{{ 'schemas.field.placeholderHint' | sqxTranslate }}
@ -14,7 +14,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldFormat">{{ 'schemas.fieldTypes.dateTime.format' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldFormat" maxlength="100" formControlName="format">
<input class="form-control" id="{{field.fieldId}}_fieldFormat" maxlength="100" formControlName="format">
<sqx-form-hint>
<span [innerHTML]="'schemas.fieldTypes.dateTime.formatHint' | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></span>

2
frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<input class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint>
{{ 'schemas.field.placeholderHint' | sqxTranslate }}

2
frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<input class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint>
{{ 'schemas.field.placeholderHint' | sqxTranslate }}

8
frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.html

@ -32,7 +32,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPattern">{{ 'common.pattern' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPattern" formControlName="pattern" placeholder="{{ 'schemas.fieldTypes.string.pattern' | sqxTranslate }}" #inputPattern autocomplete="off" autocorrect="off" autocapitalize="off" (focus)="patternsModal.show()">
<input class="form-control" id="{{field.fieldId}}_fieldPattern" formControlName="pattern" placeholder="{{ 'schemas.fieldTypes.string.pattern' | sqxTranslate }}" #inputPattern autocomplete="off" autocorrect="off" autocapitalize="off" (focus)="patternsModal.show()">
<ng-container *ngIf="settings.patterns.length > 0 && (showPatternSuggestions | async)">
<ng-container *sqxModal="patternsModal">
@ -57,7 +57,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPatternMessage">{{ 'schemas.fieldTypes.string.patternMessage' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPatternMessage" formControlName="patternMessage">
<input class="form-control" id="{{field.fieldId}}_fieldPatternMessage" formControlName="patternMessage">
</div>
</div>
@ -119,7 +119,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldDefaultValue">{{ 'schemas.field.defaultValue' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue">
<input class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue">
</div>
</div>
@ -127,7 +127,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldDefaultValues">{{ 'schemas.field.defaultValues' | sqxTranslate }}</label>
<div class="col-9">
<sqx-localized-input type="text" [languages]="languages" formControlName="defaultValues" id="{{field.fieldId}}_fieldDefaultValues"></sqx-localized-input>
<sqx-localized-input [languages]="languages" formControlName="defaultValues" id="{{field.fieldId}}_fieldDefaultValues"></sqx-localized-input>
<sqx-form-hint>
{{ 'schemas.field.defaultValuesHint' | sqxTranslate }}

2
frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.html

@ -3,7 +3,7 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-9">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<input class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint>
{{ 'schemas.field.placeholderHint' | sqxTranslate }}

4
frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html

@ -16,13 +16,13 @@
<div class="col col-name">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'schemas.previewUrls.namePlaceholder' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'schemas.previewUrls.namePlaceholder' | sqxTranslate }}">
</div>
<div class="col">
<sqx-control-errors for="url"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="url" placeholder="{{ 'schemas.previewUrls.urlPlaceholder' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="url" placeholder="{{ 'schemas.previewUrls.urlPlaceholder' | sqxTranslate }}">
</div>
<div class="col-auto">

2
frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html

@ -32,7 +32,7 @@
<div class="col">
<sqx-control-errors for="condition"></sqx-control-errors>
<input type="text" class="form-control" formControlName="condition" placeholder="{{ 'schemas.rules.condition' | sqxTranslate }}">
<input class="form-control" formControlName="condition" placeholder="{{ 'schemas.rules.condition' | sqxTranslate }}">
</div>
<div class="col-auto">

2
frontend/src/app/features/schemas/pages/schemas/schema-form.component.html

@ -18,7 +18,7 @@
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<input class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<sqx-form-hint>
{{ 'schemas.schemaNameHint' | sqxTranslate }}

2
frontend/src/app/features/settings/pages/clients/client-add-form.component.html

@ -6,7 +6,7 @@
<div class="col">
<sqx-control-errors for="id"></sqx-control-errors>
<input type="text" class="form-control" formControlName="id" maxlength="40" placeholder="{{ 'clients.clientNamePlaceholder' | sqxTranslate }}" autocomplete="off" sqxTransformInput="LowerCase">
<input class="form-control" formControlName="id" maxlength="40" placeholder="{{ 'clients.clientNamePlaceholder' | sqxTranslate }}" autocomplete="off" sqxTransformInput="LowerCase">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="addClientForm.hasNoId | async">

6
frontend/src/app/features/settings/pages/more/more-page.component.html

@ -12,7 +12,7 @@
<div class="form-group">
<label for="email">{{ 'common.name' | sqxTranslate }}</label>
<input type="text" class="form-control" disabled readonly [value]="app.name">
<input readonly class="form-control" [value]="app.name">
</div>
<div class="form-group">
@ -20,7 +20,7 @@
<sqx-control-errors for="label"></sqx-control-errors>
<input type="text" class="form-control" id="label" maxlength="100" formControlName="label">
<input class="form-control" id="label" maxlength="100" formControlName="label">
</div>
<div class="form-group">
@ -28,7 +28,7 @@
<sqx-control-errors for="description"></sqx-control-errors>
<input type="text" class="form-control" id="description" maxlength="100" formControlName="description">
<input class="form-control" id="description" maxlength="100" formControlName="description">
</div>
</div>

2
frontend/src/app/features/settings/pages/roles/role-add-form.component.html

@ -7,7 +7,7 @@
<div class="col">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="{{ 'roles.roleNamePlaceholder' | sqxTranslate }}" autocomplete="off">
<input class="form-control" formControlName="name" maxlength="40" placeholder="{{ 'roles.roleNamePlaceholder' | sqxTranslate }}" autocomplete="off">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="addRoleForm.hasNoName | async">

10
frontend/src/app/features/settings/pages/settings/settings-page.component.html

@ -29,19 +29,19 @@
<div class="col-3">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}">
</div>
<div class="col">
<sqx-control-errors for="regex"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="regex" placeholder="{{ 'common.pattern' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="regex" placeholder="{{ 'common.pattern' | sqxTranslate }}">
</div>
<div class="col-4">
<sqx-control-errors for="message"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="message" placeholder="{{ 'common.message' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="message" placeholder="{{ 'common.message' | sqxTranslate }}">
</div>
<div class="col-auto">
@ -93,13 +93,13 @@
<div class="col-3 ">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}">
</div>
<div class="col ">
<sqx-control-errors for="url"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="url" placeholder="{{ 'common.url' | sqxTranslate }}">
<input class="form-control" maxlength="1000" formControlName="url" placeholder="{{ 'common.url' | sqxTranslate }}">
</div>
<div class="col-auto">

2
frontend/src/app/features/settings/pages/workflows/workflow-add-form.component.html

@ -7,7 +7,7 @@
<div class="col">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="{{ 'workflows.workflowNamePlaceholder' | sqxTranslate }}" autocomplete="off">
<input class="form-control" formControlName="name" maxlength="40" placeholder="{{ 'workflows.workflowNamePlaceholder' | sqxTranslate }}" autocomplete="off">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="addWorkflowForm.hasNoName | async">

2
frontend/src/app/features/teams/pages/more/more-page.component.html

@ -14,7 +14,7 @@
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" id="name" maxlength="100" formControlName="name">
<input class="form-control" id="name" maxlength="100" formControlName="name">
</div>
</div>

2
frontend/src/app/framework/angular/forms/editable-title.component.html

@ -5,7 +5,7 @@
<div class="form-group me-2">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" [formControl]="renameForm" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
<input class="form-control" [formControl]="renameForm" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
</div>
</div>
<div class="col-auto">

2
frontend/src/app/framework/angular/forms/editors/autocomplete.component.html

@ -1,5 +1,5 @@
<div class="control-container">
<input type="text" class="form-control" (blur)="blur()" (keydown)="onKeyDown($event)" #input
<input class="form-control" (blur)="blur()" (keydown)="onKeyDown($event)" #input
[sqxFocusOnInit]="autoFocus"
[name]="inputName"
[placeholder]="placeholder"

4
frontend/src/app/framework/angular/forms/editors/date-time-editor.component.html

@ -9,11 +9,11 @@
</button>
</div>
<div class="input-group flex-grow">
<input type="text" class="form-control form-date" [formControl]="dateControl" placeholder="{{ 'common.date' | sqxTranslate }}"
<input class="form-control form-date" [formControl]="dateControl" placeholder="{{ 'common.date' | sqxTranslate }}"
[class.with-buttons]="isDateTimeMode && shouldShowDateTimeModeButton"
(blur)="callTouched()" maxlength="10" #dateInput>
<input type="text" class="form-control form-time" [formControl]="timeControl" placeholder="{{ 'common.time' | sqxTranslate }}" (blur)="callTouched()" *ngIf="isDateTimeMode">
<input class="form-control form-time" [formControl]="timeControl" placeholder="{{ 'common.time' | sqxTranslate }}" (blur)="callTouched()" *ngIf="isDateTimeMode">
</div>
<button type="button" class="btn btn-text-secondary btn-sm btn-clear" [class.hidden]="!hasValue"

2
frontend/src/app/framework/angular/forms/editors/dropdown.component.html

@ -1,5 +1,5 @@
<div class="selection">
<input type="text" class="form-select" [disabled]="snapshot.isDisabled" (click)="openModal()" readonly (keydown)="onKeyDown($event)" #input
<input readonly class="form-select" [disabled]="snapshot.isDisabled" (click)="openModal()" (keydown)="onKeyDown($event)" #input
autocomplete="off"
autocorrect="off"
autocapitalize="off">

2
frontend/src/app/framework/angular/forms/editors/tag-editor.component.html

@ -14,7 +14,7 @@
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>
<input type="text" class="blank text-input" #input
<input class="blank text-input" #input
(blur)="markTouched()" (copy)="onCopy($event)" (cut)="onCut($event)" (focus)="focus()" (keydown)="onKeyDown($event)" (paste)="onPaste($event)"
[name]="inputName"
[placeholder]="placeholder | sqxTranslate"

2
frontend/src/app/shared/components/app-form.component.html

@ -18,7 +18,7 @@
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<input class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<sqx-form-hint>
{{ 'apps.appNameHint' | sqxTranslate }}

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

@ -110,7 +110,7 @@
<sqx-control-errors for="fileName"></sqx-control-errors>
<input type="text" class="form-control" id="fileName" formControlName="fileName" spellcheck="false">
<input class="form-control" id="fileName" formControlName="fileName" spellcheck="false">
</div>
<div class="form-group">
@ -118,7 +118,7 @@
<sqx-control-errors for="slug"></sqx-control-errors>
<input type="text" class="form-control slug" id="slug" formControlName="slug" sqxTransformInput="Slugify" spellcheck="false">
<input class="form-control slug" id="slug" formControlName="slug" sqxTransformInput="Slugify" spellcheck="false">
<button type="button" class="btn btn-text-secondary btn-sm slug-generate" (click)="generateSlug()">
{{ 'common.generate' | sqxTranslate }}
@ -140,13 +140,13 @@
<div class="col col-name pe-1">
<sqx-control-errors for="name" fieldName="Name"></sqx-control-errors>
<input type="text" class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}" spellcheck="false">
<input class="form-control" maxlength="1000" formControlName="name" placeholder="{{ 'common.name' | sqxTranslate }}" spellcheck="false">
</div>
<div class="col pe-1">
<sqx-control-errors for="value" fieldName="Value"></sqx-control-errors>
<input type="text" class="form-control" formControlName="value" placeholder="{{ 'common.value' | sqxTranslate }}">
<input class="form-control" formControlName="value" placeholder="{{ 'common.value' | sqxTranslate }}">
</div>
<div class="col-auto col-options">

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

@ -17,7 +17,7 @@
<sqx-control-errors for="folderName"></sqx-control-errors>
<input type="text" class="form-control" id="folderName" formControlName="folderName" autocomplete="off" sqxFocusOnInit>
<input class="form-control" id="folderName" formControlName="folderName" autocomplete="off" sqxFocusOnInit>
</div>
<div class="form-group">

0
frontend/src/app/shared/components/assets/assets-selector.component.html → frontend/src/app/shared/components/assets/asset-selector.component.html

0
frontend/src/app/shared/components/assets/assets-selector.component.scss → frontend/src/app/shared/components/assets/asset-selector.component.scss

8
frontend/src/app/shared/components/assets/assets-selector.component.ts → frontend/src/app/shared/components/assets/asset-selector.component.ts

@ -21,15 +21,15 @@ interface State {
}
@Component({
selector: 'sqx-assets-selector',
styleUrls: ['./assets-selector.component.scss'],
templateUrl: './assets-selector.component.html',
selector: 'sqx-asset-selector',
styleUrls: ['./asset-selector.component.scss'],
templateUrl: './asset-selector.component.html',
providers: [
ComponentAssetsState,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit {
export class AssetSelectorComponent extends StatefulComponent<State> implements OnInit {
@Output()
public select = new EventEmitter<ReadonlyArray<AssetDto>>();

4
frontend/src/app/shared/components/contents/content-value-editor.component.html

@ -19,10 +19,10 @@
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder">
<input class="form-control" [formControlName]="field.name" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
<input class="form-control" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControlName]="field.name">

2
frontend/src/app/shared/components/forms/geolocation-editor.component.html

@ -5,7 +5,7 @@
<form [class.hidden]="snapshot.isMapHidden">
<div class="editor" #editor></div>
<input [class.hidden]="!isGoogleMaps" class="form-control search-control" type="text" [disabled]="snapshot.isDisabled" placeholder="{{ 'common.searchGoogleMaps' | sqxTranslate }}" #searchBox>
<input [class.hidden]="!isGoogleMaps" class="form-control search-control" [disabled]="snapshot.isDisabled" placeholder="{{ 'common.searchGoogleMaps' | sqxTranslate }}" #searchBox>
</form>
<div class="row mt-2">

4
frontend/src/app/shared/components/forms/markdown-editor.component.html

@ -7,9 +7,9 @@
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-assets-selector
<sqx-asset-selector
(select)="insertAssets($event)">
</sqx-assets-selector>
</sqx-asset-selector>
</ng-container>
<ng-container *sqxModal="contentsDialog">

4
frontend/src/app/shared/components/forms/rich-editor.component.html

@ -3,9 +3,9 @@
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-assets-selector
<sqx-asset-selector
(select)="insertAssets($event)">
</sqx-assets-selector>
</sqx-asset-selector>
</ng-container>
<ng-container *sqxModal="contentsDialog">

11
frontend/src/app/shared/components/references/content-selector.component.ts

@ -33,6 +33,9 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
@Input()
public schemaIds?: ReadonlyArray<string>;
@Input()
public schemaNames?: ReadonlyArray<string>;
@Input()
public language!: LanguageDto;
@ -80,10 +83,14 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
this.updateModel();
}));
this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents);
this.schemas = this.schemasState.snapshot.schemas.filter(x => x.type === 'Default' && x.canReadContents);
if (this.schemaIds && this.schemaIds.length > 0) {
this.schemas = this.schemas.filter(x => x.type === 'Default' && x.canReadContents && this.schemaIds!.includes(x.id));
this.schemas = this.schemas.filter(x => this.schemaIds!.includes(x.id));
}
if (this.schemaNames && this.schemaNames.length > 0) {
this.schemas = this.schemas.filter(x => this.schemaNames!.includes(x.name));
}
this.selectSchema(this.schemas[0]);

2
frontend/src/app/shared/components/references/reference-input.component.html

@ -1,5 +1,5 @@
<div class="input-group">
<input class="form-control" readonly [ngModel]="snapshot.selectedName" (click)="openDialog()" />
<input readonly class="form-control" [ngModel]="snapshot.selectedName" (click)="openDialog()" />
<button type="button" class="btn btn-outline-secondary" [disabled]="snapshot.isDisabled" (click)="selectContent()" sqxStopClick>
<i class="icon-close"></i>

4
frontend/src/app/shared/components/search/queries/filter-comparison.component.html

@ -69,7 +69,7 @@
</sqx-dropdown>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<input type="text" class="form-control" *ngIf="!field.schema.extra"
<input class="form-control" *ngIf="!field.schema.extra"
[ngModel]="filter.value"
(ngModelChange)="changeValue($event)" />
</ng-container>
@ -94,7 +94,7 @@
</sqx-dropdown>
</ng-container>
<ng-template #noPermission>
<input type="text" class="form-control"
<input class="form-control"
[ngModel]="filter.value"
(ngModelChange)="changeValue($event)" />
</ng-template>

2
frontend/src/app/shared/components/search/search-form.component.html

@ -120,7 +120,7 @@
<div class="form-group mt-2">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" id="appName" formControlName="name" autocomplete="off" sqxFocusOnInit>
<input class="form-control" id="appName" formControlName="name" autocomplete="off" sqxFocusOnInit>
</div>
<div class="form-check">

2
frontend/src/app/shared/components/team-form.component.html

@ -12,7 +12,7 @@
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<input class="form-control" id="name" formControlName="name" autocomplete="off" sqxTransformInput="LowerCase" sqxFocusOnInit>
<sqx-form-hint>
{{ 'teams.teamNameHint' | sqxTranslate }}

2
frontend/src/app/shared/declarations.ts

@ -13,11 +13,11 @@ export * from './components/assets/asset-folder-dropdown.component';
export * from './components/assets/asset-folder.component';
export * from './components/assets/asset-history.component';
export * from './components/assets/asset-path.component';
export * from './components/assets/asset-selector.component';
export * from './components/assets/asset-text-editor.component';
export * from './components/assets/asset-uploader.component';
export * from './components/assets/asset.component';
export * from './components/assets/assets-list.component';
export * from './components/assets/assets-selector.component';
export * from './components/assets/image-cropper.component';
export * from './components/assets/image-focus-point.component';
export * from './components/assets/pipes';

6
frontend/src/app/shared/module.ts

@ -13,7 +13,7 @@ import { MentionModule } from 'angular-mentions';
import { ChartModule } from 'angular2-chartjs';
import { NgxDocViewerModule } from 'ngx-doc-viewer';
import { SqxFrameworkModule } from '@app/framework';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations';
@NgModule({
imports: [
@ -40,8 +40,8 @@ import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCard
AssetHistoryComponent,
AssetPathComponent,
AssetPreviewUrlPipe,
AssetSelectorComponent,
AssetsListComponent,
AssetsSelectorComponent,
AssetTextEditorComponent,
AssetUploaderComponent,
AssetUploadsCountCardComponent,
@ -115,8 +115,8 @@ import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCard
AssetFolderDropdownComponent,
AssetPathComponent,
AssetPreviewUrlPipe,
AssetSelectorComponent,
AssetsListComponent,
AssetsSelectorComponent,
AssetUploaderComponent,
AssetUploadsCountCardComponent,
AssetUploadsSizeCardComponent,

3
frontend/src/app/shared/state/ui-languages.ts

@ -20,5 +20,8 @@ export module UILanguages {
}, {
iso2Code: 'zh',
localName: '简体中文',
}, {
iso2Code: 'pt',
localName: 'Portuguese',
}];
}

10
frontend/src/app/theme/_bootstrap.scss

@ -675,3 +675,13 @@ $icon-size: 4.5rem;
border-bottom-width: 1px;
}
}
.form-control {
&[readonly] {
color: $input-disabled-color;
background-color: $input-disabled-bg;
border-color: $input-disabled-border-color;
border-radius: inherit;
opacity: 1;
}
}

1034
frontend/src/app/theme/icomoon/demo.html

File diff suppressed because it is too large
Loading…
Cancel
Save