From 03e41b42faf08f8513ad9a7327df3e870899b2cd Mon Sep 17 00:00:00 2001 From: Ivan Lieckens Date: Wed, 31 Jul 2024 10:18:19 +0200 Subject: [PATCH] Initial Beta --- .editorconfig | 3 + .github/ISSUE_TEMPLATE/Bug Report.yml | 42 + .github/ISSUE_TEMPLATE/Feature Request.yml | 24 + .github/ISSUE_TEMPLATE/Question.yml | 18 + .github/pull_request_template.md | 19 + .github/workflows/Version.ps1 | 86 + .github/workflows/build.yml | 114 + .github/workflows/main.yml | 85 + .github/workflows/pullrequest.yml | 11 + CODE_OF_CONDUCT.md | 133 + Directory.Packages.props | 42 + LICENSE.md | 202 + README.md | 40 + Sitecore.AspNetCore.SDK.sln | 223 + docfx/.gitignore | 3 + docfx/README.md | 25 + docfx/docfx.json | 49 + docfx/images/Sitecore-Icon.png | Bin 0 -> 4089 bytes docfx/images/Sitecore-Logo.svg | 19 + docfx/index.md | 10 + docfx/overview/index.md | 83 + docfx/template/public/main.css | 7 + docfx/toc.yml | 6 + nuget.config | 10 + src/Directory.Build.props | 31 + src/GlobalSuppressions.cs | 13 + .../ExperienceEditorMarkerService.cs | 6 + .../Configuration/ExperienceEditorOptions.cs | 25 + .../ExperienceEditorConstants.cs | 20 + ...erienceEditorAppConfigurationExtensions.cs | 60 + .../ExperienceEditorOptionsExtensions.cs | 27 + .../Mappers/SitecoreLayoutResponseMapper.cs | 50 + .../Middleware/ExperienceEditorMiddleware.cs | 219 + .../Models/ExperienceEditorPostModel.cs | 53 + .../Properties/Resources.Designer.cs | 72 + .../Properties/Resources.resx | 123 + ...ore.AspNetCore.SDK.ExperienceEditor.csproj | 33 + .../TagHelpers/ChromeDataBuilder.cs | 120 + .../TagHelpers/ChromeDataSerializer.cs | 18 + .../TagHelpers/EditFrameTagHelper.cs | 122 + .../TagHelpers/IChromeDataBuilder.cs | 18 + .../TagHelpers/IChromeDataSerializer.cs | 14 + .../TagHelpers/Model/ChromeCommand.cs | 37 + .../Model/DefaultEditFrameButton.cs | 84 + .../Model/DefaultEditFrameButtonIds.cs | 12 + .../TagHelpers/Model/DividerEditButton.cs | 13 + .../TagHelpers/Model/EditButtonBase.cs | 22 + .../TagHelpers/Model/EditFrameDataSource.cs | 22 + .../TagHelpers/Model/FieldEditButton.cs | 12 + .../TagHelpers/Model/WebEditButton.cs | 22 + .../Models/SitecoreGraphQLClientOptions.cs | 26 + .../InvalidGraphQLConfigurationException.cs | 33 + .../GraphQlConfigurationExtensions.cs | 57 + .../Sitecore.AspNetCore.SDK.GraphQL.csproj | 14 + .../HttpLayoutRequestHandlerOptions.cs | 14 + .../SitecoreLayoutClientBuilder.cs | 16 + .../SitecoreLayoutClientOptions.cs | 19 + .../SitecoreLayoutRequestHandlerBuilder.cs | 32 + .../SitecoreLayoutRequestOptions.cs | 20 + .../SitecoreLayoutServiceMarkerService.cs | 6 + .../DefaultLayoutClient.cs | 113 + ...actSitecoreLayoutServiceClientException.cs | 45 + .../Exceptions/FieldReaderException.cs | 47 + ...estSitecoreLayoutServiceClientException.cs | 45 + ...nseSitecoreLayoutServiceClientException.cs | 45 + ...undSitecoreLayoutServiceClientException.cs | 45 + .../SitecoreLayoutServiceClientException.cs | 45 + ...outServiceMessageConfigurationException.cs | 45 + .../SitecoreLayoutServiceServerException.cs | 45 + .../Extensions/DictionaryExtensions.cs | 20 + .../JsonSerializerOptionsExtensions.cs | 29 + .../LayoutRequestHandlerBuilderExtensions.cs | 110 + .../Extensions/ServiceCollectionExtensions.cs | 50 + .../SitecoreLayoutClientBuilderExtensions.cs | 313 ++ .../SitecoreLayoutRequestExtensions.cs | 85 + .../Interfaces/ILayoutRequestHandler.cs | 18 + .../ILayoutRequestHandlerBuilder.cs | 21 + .../Interfaces/IMapRequest.cs | 16 + .../Interfaces/ISitecoreLayoutClient.cs | 17 + .../ISitecoreLayoutClientBuilder.cs | 14 + .../LayoutServiceConstants.cs | 45 + .../Properties/Resources.Designer.cs | 207 + .../Properties/Resources.resx | 174 + .../GraphQL/GraphQlLayoutServiceHandler.cs | 100 + .../Request/Handlers/GraphQL/ItemModel.cs | 14 + .../Request/Handlers/GraphQL/LayoutModel.cs | 12 + .../Handlers/GraphQL/LayoutQueryResponse.cs | 12 + .../GraphQL/LayoutServiceGraphqlException.cs | 18 + .../Handlers/HttpLayoutRequestHandler.cs | 180 + .../Request/RequestKeys.cs | 52 + .../Request/SitecoreLayoutRequest.cs | 68 + .../SitecoreLayoutRequestExtensions.cs | 244 + .../Response/Model/Component.cs | 41 + .../Response/Model/Context.cs | 32 + .../Response/Model/EditableChrome.cs | 33 + .../Response/Model/EditableField.cs | 17 + .../Response/Model/Field.cs | 20 + .../Response/Model/FieldOfTValue.cs | 13 + .../Response/Model/FieldReader.cs | 76 + .../Response/Model/Fields/CheckboxField.cs | 28 + .../Response/Model/Fields/ContentListField.cs | 92 + .../Fields/ContentListFieldOfTTargetModel.cs | 87 + .../Response/Model/Fields/DateField.cs | 29 + .../Response/Model/Fields/FileField.cs | 28 + .../Response/Model/Fields/HyperLinkField.cs | 28 + .../Response/Model/Fields/ImageField.cs | 28 + .../Response/Model/Fields/ItemLinkField.cs | 95 + .../Fields/ItemLinkFieldOfTTargetModel.cs | 22 + .../Response/Model/Fields/NumberField.cs | 27 + .../Response/Model/Fields/RichTextField.cs | 36 + .../Response/Model/Fields/TextField.cs | 29 + .../Response/Model/FieldsReader.cs | 167 + .../Response/Model/IEditableField.cs | 12 + .../Response/Model/IField.cs | 6 + .../Response/Model/IFieldReader.cs | 41 + .../Response/Model/IFieldsReader.cs | 80 + .../Response/Model/IPlaceholderFeature.cs | 6 + .../Response/Model/IValueField.cs | 13 + .../Response/Model/IWrappedEditableField.cs | 17 + .../Response/Model/Placeholder.cs | 6 + .../Response/Model/PlaceholderExtensions.cs | 52 + .../Model/Presentation/CachingData.cs | 66 + .../Response/Model/Presentation/Device.cs | 38 + .../Model/Presentation/Personalization.cs | 38 + .../Model/Presentation/PlaceholderData.cs | 31 + .../Response/Model/Presentation/Rendering.cs | 59 + .../Response/Model/Properties/File.cs | 52 + .../Response/Model/Properties/HyperLink.cs | 50 + .../Response/Model/Properties/Image.cs | 52 + .../Response/Model/Route.cs | 62 + .../Response/Model/Site.cs | 12 + .../Response/Model/SitecoreData.cs | 24 + .../Response/Model/WrappedEditableField.cs | 22 + .../Response/PageState.cs | 22 + .../Response/SitecoreLayoutResponse.cs | 59 + .../Response/SitecoreLayoutResponseContent.cs | 19 + .../Constants/ComponentPropertiesNames.cs | 37 + .../EditableChromePropertiesNames.cs | 27 + .../Serialization/Converter/FieldConverter.cs | 37 + .../Serialization/Converter/FieldParser.cs | 39 + .../Serialization/Converter/IFieldParser.cs | 17 + .../Converter/PlaceholderFeatureConverter.cs | 197 + .../Fields/JsonSerializedField.cs | 29 + .../ISitecoreLayoutSerializer.cs | 16 + .../JsonLayoutServiceSerializer.cs | 62 + ...AspNetCore.SDK.LayoutService.Client.csproj | 41 + .../UseSitecoreRenderingAttribute.cs | 23 + .../SitecoreComponentFieldAttribute.cs | 20 + .../SitecoreComponentFieldsAttribute.cs | 15 + .../SitecoreComponentParameterAttribute.cs | 20 + .../SitecoreComponentPropertyAttribute.cs | 20 + .../Attributes/SitecoreContextAttribute.cs | 15 + .../SitecoreContextPropertyAttribute.cs | 20 + .../SitecoreLayoutResponseAttribute.cs | 15 + .../Attributes/SitecoreRouteFieldAttribute.cs | 20 + .../SitecoreRouteFieldsAttribute.cs | 15 + .../SitecoreRoutePropertyAttribute.cs | 20 + .../Binding/Extensions/BindingExtensions.cs | 62 + .../Binding/IViewModelBinder.cs | 48 + ...ecoreLayoutComponentModelBinderProvider.cs | 31 + ...itecoreLayoutContextModelBinderProvider.cs | 23 + ...tecoreLayoutResponseModelBinderProvider.cs | 20 + .../SitecoreLayoutRouteModelBinderProvider.cs | 29 + .../Binding/SitecoreLayoutModelBinder.cs | 66 + .../Binding/SitecoreViewModelBinder.cs | 75 + .../Sources/SitecoreLayoutBindingSource.cs | 101 + .../SitecoreLayoutComponentBindingSource.cs | 36 + ...tecoreLayoutComponentFieldBindingSource.cs | 51 + ...ecoreLayoutComponentFieldsBindingSource.cs | 38 + ...reLayoutComponentParameterBindingSource.cs | 45 + ...oreLayoutComponentPropertyBindingSource.cs | 33 + .../SitecoreLayoutContextBindingSource.cs | 50 + ...ecoreLayoutContextPropertyBindingSource.cs | 62 + .../SitecoreLayoutResponseBindingSource.cs | 38 + .../SitecoreLayoutRouteBindingSource.cs | 37 + .../SitecoreLayoutRouteFieldBindingSource.cs | 43 + .../SitecoreLayoutRouteFieldsBindingSource.cs | 39 + ...itecoreLayoutRoutePropertyBindingSource.cs | 36 + .../Configuration/ForwardHeadersOptions.cs | 29 + .../Configuration/MultisiteOptions.cs | 17 + .../RenderingEngineMarkerService.cs | 6 + .../Configuration/RenderingEngineOptions.cs | 64 + .../SitecoreRenderingEngineBuilder.cs | 16 + .../ApplicationBuilderExtensions.cs | 43 + .../ControllerEndpointExtensions.cs | 32 + .../Extensions/EncodingExtensions.cs | 53 + .../Extensions/ForwardHeadersMarkerService.cs | 6 + .../Extensions/HttpContextExtensions.cs | 160 + .../Extensions/HttpRequestExtensions.cs | 53 + .../MultisiteAppConfigurationExtensions.cs | 55 + .../Extensions/RazorPageExtensions.cs | 48 + .../RenderingEngineOptionsExtensions.cs | 306 ++ .../RequestLocalizationOptionsExtensions.cs | 28 + .../Extensions/ServiceCollectionExtensions.cs | 157 + .../Extensions/SitecoreFieldExtensions.cs | 68 + .../SitecoreLayoutContextControllerFilter.cs | 30 + .../ISitecoreLayoutRequestMapper.cs | 17 + .../Interfaces/ISitecoreRenderingContext.cs | 32 + .../ISitecoreRenderingEngineBuilder.cs | 14 + .../SitecoreQueryStringCultureProvider.cs | 40 + .../Mappers/SitecoreLayoutRequestMapper.cs | 46 + .../Middleware/Models/Site.cs | 12 + .../Middleware/Models/SiteInfo.cs | 17 + .../Models/SiteInfoCollectionResult.cs | 12 + .../Middleware/MultisiteMiddleware.cs | 87 + .../Middleware/RenderingEngineMiddleware.cs | 93 + .../Middleware/RenderingEnginePipeline.cs | 20 + .../Properties/Resources.Designer.cs | 288 ++ .../Properties/Resources.resx | 206 + .../Rendering/ComponentRendererDescriptor.cs | 44 + .../Rendering/ComponentRendererFactory.cs | 51 + .../Rendering/EditableChromeRenderer.cs | 29 + .../Rendering/IComponentRenderer.cs | 19 + .../Rendering/IComponentRendererFactory.cs | 16 + .../Rendering/IEditableChromeRenderer.cs | 17 + .../Rendering/LoggingComponentRenderer.cs | 47 + ...odelBoundViewComponentComponentRenderer.cs | 42 + .../Rendering/PartialViewComponentRenderer.cs | 51 + .../Rendering/RenderingHelpers.cs | 25 + .../Rendering/SitecoreRenderingContext.cs | 22 + .../ViewComponentComponentRenderer.cs | 54 + .../RenderingEngineConstants.cs | 124 + .../Routing/LanguageRouteConstraint.cs | 32 + .../Services/GraphQlSiteCollectionService.cs | 59 + .../Services/ISiteCollectionService.cs | 15 + .../Services/ISiteResolver.cs | 14 + .../Services/SiteResolver.cs | 81 + ...core.AspNetCore.SDK.RenderingEngine.csproj | 54 + .../TagHelpers/ComponentHolder.cs | 35 + .../TagHelpers/Fields/DateTagHelper.cs | 73 + .../TagHelpers/Fields/FileTagHelper.cs | 90 + .../TagHelpers/Fields/ImageTagHelper.cs | 225 + .../TagHelpers/Fields/LinkTagHelper.cs | 203 + .../TagHelpers/Fields/NumberTagHelper.cs | 75 + .../TagHelpers/Fields/RichTextTagHelper.cs | 58 + .../TagHelpers/Fields/TextFieldTagHelper.cs | 58 + .../TagHelpers/PlaceholderTagHelper.cs | 136 + .../ViewComponents/BindingViewComponent.cs | 89 + .../SitecoreComponentViewComponent.cs | 29 + ...coreRedirectsAppConfigurationExtensions.cs | 47 + .../SitemapAppConfigurationExtensions.cs | 107 + .../Models/RedirectInfo.cs | 27 + .../Models/RedirectType.cs | 23 + .../Models/Site.cs | 12 + .../Models/SiteInfo.cs | 17 + .../Models/SiteInfoResultModel.cs | 12 + .../Models/SitemapOptions.cs | 12 + .../Middleware/SitecoreRewriteMiddleware.cs | 152 + .../Models/SitecoreRewriteOptions.cs | 17 + .../Redirects/Rules/SitecoreRedirectRule.cs | 129 + .../Redirects/Rules/SitecoreRewriteRule.cs | 112 + .../Services/GraphQlSiteInfoService.cs | 134 + .../Services/IRedirectsService.cs | 16 + .../Services/ISitemapService.cs | 15 + .../Services/SitemapService.cs | 28 + ...e.AspNetCore.SDK.SearchOptimization.csproj | 33 + .../Providers/DateTimeProvider.cs | 11 + .../Providers/IDateTimeProvider.cs | 13 + ....SDK.Tracking.VisitorIdentification.csproj | 20 + .../SitecoreVisitorIdentificationOptions.cs | 13 + .../SitecoreVisitorIdentificationTagHelper.cs | 75 + ...dentificationAppConfigurationExtensions.cs | 72 + .../VisitorIdentificationConstants.cs | 20 + .../Sitecore.AspNetCore.SDK.Tracking.csproj | 12 + .../TrackingAppConfigurationExtensions.cs | 50 + src/icon.png | Bin 0 -> 8968 bytes tests/Directory.Build.props | 29 + .../ActionProviders/ISetupActionsProvider.cs | 8 + .../ActionProviders/SetupActionsProvider.cs | 37 + .../StaticMethodSetupActionsProvider.cs | 15 + .../StaticPropertySetupActionsProvider.cs | 15 + .../AutoNSubstituteDataAttribute.cs | 25 + .../Attributes/AutoSetupDataAttribute.cs | 149 + .../InlineAutoNSubstituteDataAttribute.cs | 20 + .../InlineAutoSetupDataAttribute.cs | 28 + .../MemberAutoNSubstituteDataAttribute.cs | 29 + .../MemberAutoSetupDataAttribute.cs | 61 + .../GuardClauseAssertionExtensions.cs | 30 + ...Sitecore.AspNetCore.SDK.AutoFixture.csproj | 19 + .../ExperienceEditorOptionsFixture.cs | 16 + .../ApplicationBuilderExtensionsFixture.cs | 39 + .../ServiceCollectionExtensionsFixture.cs | 18 + .../SitecoreLayoutResponseMapperFixture.cs | 69 + .../ExperienceEditorMiddlewareFixture.cs | 143 + .../ExperienceEditorPostModelFixture.cs | 50 + ...pNetCore.SDK.ExperienceEditor.Tests.csproj | 12 + .../TagHelpers/ChromeDataBuilderFixture.cs | 138 + .../TagHelpers/ChromeDataSerializerFixture.cs | 41 + .../TagHelpers/EditFrameTagHelperFixture.cs | 261 ++ .../GraphQlConfigurationExtensionsFixture.cs | 74 + ...tecore.AspNetCore.SDK.GraphQL.Tests.csproj | 15 + .../ContextFixture.cs | 42 + .../DeviceFixture.cs | 60 + .../EditableChromeFixture.cs | 142 + .../FieldsFixture.cs | 496 +++ .../MockModels/LinkFieldModel.cs | 8 + ...outService.Client.Integration.Tests.csproj | 22 + .../HttpLayoutRequestHandlerOptionsFixture.cs | 19 + .../SitecoreLayoutClientBuilderFixture.cs | 31 + ...youtRequestHandlerBuilderTClientFixture.cs | 52 + .../SitecoreLayoutRequestOptionsFixture.cs | 19 + .../SitecoreLayoutServiceOptionsFixture.cs | 19 + .../DefaultLayoutClientFixture.cs | 450 ++ ...coreLayoutServiceClientExceptionFixture.cs | 59 + .../Exceptions/FieldReaderExceptionFixture.cs | 61 + ...coreLayoutServiceClientExceptionFixture.cs | 59 + ...coreLayoutServiceClientExceptionFixture.cs | 59 + ...coreLayoutServiceClientExceptionFixture.cs | 59 + ...coreLayoutServiceClientExceptionFixture.cs | 59 + ...iceMessageConfigurationExceptionFixture.cs | 59 + ...coreLayoutServiceServerExceptionFixture.cs | 59 + .../Extensions/DictionaryExtensionsFixture.cs | 46 + ...tRequestHandlerBuilderExtensionsFixture.cs | 328 ++ ...ecoreLayoutRequestHttpExtensionsFixture.cs | 147 + .../ServiceCollectionExtensionsFixture.cs | 36 + ...oreLayoutClientBuilderExtensionsFixture.cs | 174 + ...tRequestHandlerBuilderExtensionsFixture.cs | 96 + ...estAbstractSitecoreLayoutRequestHandler.cs | 11 + .../HttpLayoutRequestHandlerWrapper.cs | 36 + .../MockModels/HttpMessageHandlerWrapper.cs | 12 + .../MockModels/HttpResponseMessageWrapper.cs | 10 + .../GraphQlLayoutServiceHandlerFixture.cs | 229 + .../HttpLayoutRequestHandlerFixture.cs | 443 ++ .../SitecoreLayoutRequestExtensionsFixture.cs | 866 ++++ .../Request/SitecoreLayoutRequestFixture.cs | 17 + .../Response/Model/ComponentFixture.cs | 33 + .../Response/Model/ContextFixture.cs | 21 + .../Response/Model/EditableChromeFixture.cs | 21 + .../Response/Model/FieldFixtureOfTField.cs | 97 + .../Response/Model/FieldOfTValueFixture.cs | 54 + .../Model/Fields/CheckBoxFieldFixture.cs | 32 + .../Model/Fields/ContentListFieldFixture.cs | 18 + .../Model/Fields/ItemLinkFieldFixture.cs | 20 + .../Response/Model/FieldsReaderFixture.cs | 288 ++ .../Model/PlaceholderExtensionsFixture.cs | 104 + .../Response/Model/PlaceholderFixture.cs | 18 + .../Model/Presentation/CachingDataFixture.cs | 25 + .../Model/Presentation/DeviceFixture.cs | 21 + .../Presentation/PersonalizationFixture.cs | 21 + .../Presentation/PlaceholderDataFixture.cs | 20 + .../Model/Presentation/RenderingFixture.cs | 24 + .../Response/Model/RouteFixture.cs | 36 + .../Response/Model/SimpleTestModel.cs | 11 + .../Response/Model/SiteFixture.cs | 18 + .../Response/Model/SitecoreDataFixture.cs | 20 + .../SitecoreLayoutResponseContentFixture.cs | 18 + .../Response/SitecoreLayoutResponseFixture.cs | 48 + .../Converter/FieldConverterTests.cs | 139 + .../Converter/FieldParserTests.cs | 67 + .../PlaceholderFeatureConverterTests.cs | 256 ++ .../Fields/JsonSerializedFieldTests.cs | 126 + .../JsonLayoutServiceSerializerFixture.cs | 48 + ...Core.SDK.LayoutService.Client.Tests.csproj | 31 + .../ExperienceEditorMiddlewareBenchmarks.cs | 83 + .../Program.cs | 21 + .../RenderingEngineBenchmarks.cs | 76 + ...Core.SDK.RenderingEngine.Benchmarks.csproj | 22 + .../TrackingBenchmarks.cs | 125 + .../ComponentModels/ComplexComponent.cs | 35 + .../ComponentModels/Component2.cs | 10 + .../ComponentModels/Component3.cs | 16 + .../ComponentModels/Component4.cs | 18 + .../ComponentModels/Component5.cs | 16 + .../ComponentModels/Component6.cs | 8 + .../ComponentWithAllFieldTypes.cs | 24 + .../ComponentModels/ComponentWithDates.cs | 10 + .../ComponentModels/ComponentWithFiles.cs | 8 + .../ComponentModels/ComponentWithImages.cs | 12 + .../ComponentModels/ComponentWithLinks.cs | 12 + .../ComponentWithMissingData.cs | 19 + .../ComponentWithNestedComponent.cs | 12 + .../ComponentModels/ComponentWithNumber.cs | 10 + .../ComponentModels/ComponentWithoutId.cs | 15 + .../ComponentModels/CustomClass.cs | 11 + .../ComponentModels/CustomContext.cs | 12 + .../CustomContextIndividual.cs | 10 + .../CustomModelContextComponent.cs | 17 + .../ComponentModels/RouteFields.cs | 10 + .../ComponentModels/TestClass1.cs | 10 + .../ComponentModels/TestClass2.cs | 8 + .../ControllerMiddlewareController.cs | 16 + .../Controllers/ErrorController.cs | 19 + .../Controllers/GlobalMiddlewareController.cs | 62 + .../Controllers/HomeController.cs | 53 + .../Controllers/MultisiteController.cs | 17 + .../Controllers/TrackingController.cs | 22 + .../Controllers/UsingAttributeController.cs | 14 + .../UsingGlobalMiddlewareController.cs | 8 + .../Binding/ComplexModelBindingFixture.cs | 109 + .../CustomModelContextBindingFixture.cs | 115 + .../ModelBindingErrorHandlingFixture.cs | 98 + .../Fixtures/Binding/ModelBindingFixture.cs | 103 + .../Binding/ViewFieldsBindingFixture.cs | 89 + .../Fixtures/ControllerMiddlewareFixture.cs | 106 + .../LoggingComponentRendererFixture.cs | 82 + .../MultipleComponentsAddedFixture.cs | 90 + .../PartialViewRendererFixture.cs | 79 + .../Fixtures/ErrorHandlingFixture.cs | 419 ++ .../ExperienceEditorCustomRoutingFixture.cs | 79 + .../ExperienceEditorFixture.cs | 560 +++ ...xperienceEditorParsePathByItemIdFixture.cs | 136 + .../ForwardHeadersToLayoutServiceFixture.cs | 247 ++ .../Fixtures/GlobalMiddlewareFixture.cs | 120 + .../AdvanceLocalizationFixture.cs | 140 + .../DefaultLocalizationFixture.cs | 81 + .../Localization/LocalizationFixture.cs | 86 + ...lizationUsingAttributeMiddlewareFixture.cs | 148 + .../Fixtures/Mocks/CustomHttpClientFactory.cs | 10 + .../Fixtures/Multisite/MultisiteFixture.cs | 181 + .../RequestDefaultsConfigurationFixture.cs | 217 + .../Fixtures/RequestExtensionsFixture.cs | 123 + .../RequestHeadersValidationFixture.cs | 97 + .../Fixtures/RequestMappingFixture.cs | 181 + .../EdgeSitemapProxyFixture.cs | 95 + .../SitecoreRewriteFixture.cs | 273 ++ .../SearchOptimization/SitemapProxyFixture.cs | 67 + ...oreLayoutClientBuilderExtensionsFixture.cs | 56 + .../TagHelpers/AllFieldTagHelpersFixture.cs | 97 + .../TagHelpers/DateFieldTagHelperFixture.cs | 107 + .../TagHelpers/FileFieldTagHelperFixture.cs | 245 + .../TagHelpers/ImageFieldTagHelperFixture.cs | 184 + .../TagHelpers/LinkFieldTagHelperFixture.cs | 282 ++ .../TagHelpers/NumberFieldTagHelperFixture.cs | 108 + .../TagHelpers/PlaceholderTagHelperFixture.cs | 129 + .../RichTextFieldTagHelperFixture.cs | 149 + .../TagHelpers/TextFieldTagHelperFixture.cs | 128 + .../Tracking/AttributeBasedTrackingFixture.cs | 144 + .../Fixtures/Tracking/TrackingFixture.cs | 206 + .../Fixtures/Tracking/TrackingProxyFixture.cs | 97 + .../HttpLayoutClientMessageHandler.cs | 18 + .../Logging/InMemoryLog.cs | 6 + .../Logging/InMemoryLogger.cs | 32 + .../Logging/IntegrationTestLoggerProvider.cs | 11 + .../Properties/launchSettings.json | 12 + ...K.RenderingEngine.Integration.Tests.csproj | 21 + .../TestServerBuilder.cs | 78 + .../ViewComponents/Component1ViewComponent.cs | 14 + .../WithBoundSitecoreContextViewModel.cs | 12 + .../WithBoundSitecoreRouteViewModel.cs | 13 + .../WithBoundSitecoreContext.cshtml | 5 + .../WithBoundSitecoreResponse.cshtml | 3 + .../WithBoundSitecoreRoute.cshtml | 5 + .../Components/Component1/Default.cshtml | 3 + .../SitecoreComponent/ComplexComponent.cshtml | 16 + .../SitecoreComponent/Component2.cshtml | 8 + .../SitecoreComponent/Component3.cshtml | 9 + .../SitecoreComponent/Component4.cshtml | 11 + .../SitecoreComponent/Component4.da.cshtml | 11 + .../SitecoreComponent/Component5.cshtml | 9 + .../SitecoreComponent/Component6.cshtml | 7 + .../ComponentWithAllFieldTypes.cshtml | 13 + .../ComponentWithDates.cshtml | 9 + .../ComponentWithFIles.cshtml | 23 + .../ComponentWithImages.cshtml | 7 + .../ComponentWithLinks.cshtml | 11 + .../ComponentWithMissingData.cshtml | 10 + .../ComponentWithNumber.cshtml | 11 + .../ComponentWithoutId.cshtml | 6 + .../CustomModelContextComponent.cshtml | 15 + .../Views/Shared/Index.cshtml | 1 + .../Shared/NestedPlaceholderPageLayout.cshtml | 2 + .../Views/Shared/RelativeUrls.cshtml | 2 + .../Views/Shared/ViewForHandlingError.cshtml | 1 + .../Shared/ViewWithMissingComponent.cshtml | 2 + .../Shared/VisitorIdentificationLayout.cshtml | 3 + .../Views/Shared/_ComponentNotFound.cshtml | 1 + .../Views/Shared/_PartialView.cshtml | 7 + .../Views/_ViewImports.cshtml | 4 + .../appsettings.Development.json | 9 + .../appsettings.json | 10 + .../UseSitecoreRenderingAttributeFixture.cs | 30 + .../SitecoreComponentFieldAttributeFixture.cs | 25 + ...SitecoreComponentFieldsAttributeFixture.cs | 17 + ...ecoreComponentParameterAttributeFixture.cs | 25 + ...tecoreComponentPropertyAttributeFixture.cs | 25 + .../SitecoreContextAttributeFixture.cs | 17 + ...SitecoreContextPropertyAttributeFixture.cs | 25 + .../SitecoreLayoutResponseAttributeFixture.cs | 17 + .../SitecoreRouteFieldAttributeFixture.cs | 25 + .../SitecoreRouteFieldsAttributeFixture.cs | 17 + .../SitecoreRoutePropertyAttributeFixture.cs | 25 + .../Binding/BindingViewComponentFixture.cs | 116 + .../Extensions/BindingExtensionsFixture.cs | 135 + ...youtComponentModelBinderProviderFixture.cs | 110 + ...LayoutContextModelBinderProviderFixture.cs | 122 + ...ayoutResponseModelBinderProviderFixture.cs | 76 + ...reLayoutRouteModelBinderProviderFixture.cs | 167 + .../SitecoreLayoutModelBinderFixture.cs | 72 + .../Binding/SitecoreViewModelBinderFixture.cs | 150 + ...coreLayoutComponentBindingSourceFixture.cs | 126 + ...ayoutComponentFieldBindingSourceFixture.cs | 320 ++ ...youtComponentFieldsBindingSourceFixture.cs | 139 + ...tComponentParameterBindingSourceFixture.cs | 253 ++ ...utComponentPropertyBindingSourceFixture.cs | 247 ++ ...tecoreLayoutContextBindingSourceFixture.cs | 243 + ...youtContextPropertyBindingSourceFixture.cs | 201 + ...ecoreLayoutResponseBindingSourceFixture.cs | 104 + ...SitecoreLayoutRouteBindingSourceFixture.cs | 149 + ...oreLayoutRouteFieldBindingSourceFixture.cs | 240 + ...reLayoutRouteFieldsBindingSourceFixture.cs | 135 + ...LayoutRoutePropertyBindingSourceFixture.cs | 201 + .../RenderingEngineOptionsFixture.cs | 81 + .../ApplicationBuilderExtensionsFixture.cs | 85 + .../Extensions/EncodingExtensionsFixture.cs | 69 + .../HttpContextExtensionsFixture.cs | 43 + .../HttpRequestExtensionsFixture.cs | 99 + ...tisiteAppConfigurationExtensionsFixture.cs | 72 + .../Extensions/RazorPageExtensionsFixture.cs | 108 + ...RenderingEngineOptionsExtensionsFixture.cs | 330 ++ .../ServiceCollectionExtensionsFixture.cs | 41 + .../SitecoreLayoutControllerFilterFixture.cs | 59 + .../Middleware/MultisiteMiddlewareFixture.cs | 114 + .../RenderingEngineMiddlewareFixture.cs | 115 + .../RenderingEnginePipelineFixture.cs | 48 + .../Mocks/BindingViewComponentMock.cs | 10 + .../Mocks/ContentBlock.cs | 10 + .../Mocks/HeaderBlock.cs | 10 + .../Mocks/ICustomHtmlHelper.cs | 6 + .../Mocks/ICustomViewComponentHelper.cs | 6 + .../Mocks/TestBindingSource.cs | 6 + .../Mocks/TestComponentRenderer.cs | 19 + .../Mocks/TestModelBinderProviderContext.cs | 24 + .../Mocks/TestModelMetadata.cs | 87 + .../Mocks/TestPlaceholderFeature.cs | 8 + .../Mocks/TestSitecoreLayoutBindingSource.cs | 14 + .../ComponentRendererDescriptorFixture.cs | 50 + .../ComponentRendererFactoryFixture.cs | 106 + .../EditableChromeRendererFixture.cs | 76 + .../LoggingComponentRendererFixture.cs | 66 + ...ndViewComponentComponentRendererFixture.cs | 146 + .../PartialViewComponentRendererFixture.cs | 77 + .../SitecoreRenderingContextFixture.cs | 28 + .../ViewComponentComponentRendererFixture.cs | 87 + .../Routing/LanguageRouteConstraintFixture.cs | 47 + .../Services/SiteResolverFixture.cs | 125 + ...spNetCore.SDK.RenderingEngine.Tests.csproj | 12 + .../SitecoreLayoutRequestMapperFixture.cs | 88 + .../TagHelpers/ComponentHolderFixture.cs | 42 + .../TagHelpers/Fields/DateTagHelperFixture.cs | 347 ++ .../TagHelpers/Fields/FileTagHelperFixture.cs | 184 + .../Fields/ImageTagHelperFixture.cs | 721 +++ .../TagHelpers/Fields/LinkTagHelperFixture.cs | 795 ++++ .../Fields/NumberTagHelperFixture.cs | 311 ++ .../Fields/RichTextTagHelperFixture.cs | 717 +++ .../Fields/TextFieldTagHelperFixture.cs | 203 + .../TagHelpers/PlaceholderTagHelperFixture.cs | 585 +++ .../SitecoreComponentViewComponentFixture.cs | 89 + ...plicationConfigurationExtensionsFixture.cs | 28 + ...ificationConfigurationExtensionsFixture.cs | 69 + ...ecore.AspNetCore.SDK.Tracking.Tests.csproj | 12 + ...reVisitorIdentificationTagHelperFixture.cs | 279 ++ tests/Tests.GlobalSuppressions.cs | 19 + .../CannedResponses.cs | 3922 +++++++++++++++++ .../Models/CustomFieldType.cs | 19 + .../Serializer.cs | 23 + .../Sitecore.AspNetCore.SDK.TestData.csproj | 10 + .../TestConstants.cs | 83 + tests/data/json/devices.json | 48 + tests/data/json/edit-in-horizon-mode.json | 1870 ++++++++ tests/data/json/edit.json | 1837 ++++++++ tests/data/json/layoutResponse.json | 1 + .../data/json/mixedComponentsEditChromes.json | 75 + tests/data/json/onlyComponents.json | 20 + tests/data/json/onlyEditChromes.json | 57 + 564 files changed, 46477 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/Bug Report.yml create mode 100644 .github/ISSUE_TEMPLATE/Feature Request.yml create mode 100644 .github/ISSUE_TEMPLATE/Question.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/Version.ps1 create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pullrequest.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Directory.Packages.props create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Sitecore.AspNetCore.SDK.sln create mode 100644 docfx/.gitignore create mode 100644 docfx/README.md create mode 100644 docfx/docfx.json create mode 100644 docfx/images/Sitecore-Icon.png create mode 100644 docfx/images/Sitecore-Logo.svg create mode 100644 docfx/index.md create mode 100644 docfx/overview/index.md create mode 100644 docfx/template/public/main.css create mode 100644 docfx/toc.yml create mode 100644 nuget.config create mode 100644 src/Directory.Build.props create mode 100644 src/GlobalSuppressions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorMarkerService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/ExperienceEditorConstants.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorOptionsExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Mappers/SitecoreLayoutResponseMapper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Middleware/ExperienceEditorMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Models/ExperienceEditorPostModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.Designer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.resx create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/Sitecore.AspNetCore.SDK.ExperienceEditor.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataSerializer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/EditFrameTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataSerializer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/ChromeCommand.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButton.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButtonIds.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DividerEditButton.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditButtonBase.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditFrameDataSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/FieldEditButton.cs create mode 100644 src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/WebEditButton.cs create mode 100644 src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.GraphQL/Sitecore.AspNetCore.SDK.GraphQL.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/HttpLayoutRequestHandlerOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestHandlerBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutServiceMarkerService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/DefaultLayoutClient.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/CouldNotContactSitecoreLayoutServiceClientException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/FieldReaderException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidRequestSitecoreLayoutServiceClientException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidResponseSitecoreLayoutServiceClientException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/ItemNotFoundSitecoreLayoutServiceClientException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceClientException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceMessageConfigurationException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceServerException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/DictionaryExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/JsonSerializerOptionsExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/LayoutRequestHandlerBuilderExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutRequestExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandler.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandlerBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/IMapRequest.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClient.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClientBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/LayoutServiceConstants.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.Designer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.resx create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/ItemModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutQueryResponse.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/HttpLayoutRequestHandler.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/RequestKeys.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequest.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequestExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Component.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Context.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableChrome.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Field.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldOfTValue.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldReader.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/CheckboxField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListFieldOfTTargetModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/DateField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/FileField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/HyperLinkField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ImageField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkFieldOfTTargetModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/NumberField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/RichTextField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/TextField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldsReader.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldReader.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldsReader.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IPlaceholderFeature.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IValueField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IWrappedEditableField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Placeholder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/PlaceholderExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/CachingData.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Device.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Personalization.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/PlaceholderData.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Rendering.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/File.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/HyperLink.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/Image.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Route.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Site.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/SitecoreData.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/PageState.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponse.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponseContent.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/ComponentPropertiesNames.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/EditableChromePropertiesNames.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldConverter.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/IFieldParser.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/PlaceholderFeatureConverter.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Fields/JsonSerializedField.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/ISitecoreLayoutSerializer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/JsonLayoutServiceSerializer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Sitecore.AspNetCore.SDK.LayoutService.Client.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Attributes/UseSitecoreRenderingAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldsAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentParameterAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentPropertyAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextPropertyAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreLayoutResponseAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldsAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRoutePropertyAttribute.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Extensions/BindingExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/IViewModelBinder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutComponentModelBinderProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutContextModelBinderProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutResponseModelBinderProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutRouteModelBinderProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreLayoutModelBinder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreViewModelBinder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldsBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentParameterBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentPropertyBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextPropertyBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutResponseBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldsBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRoutePropertyBindingSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/ForwardHeadersOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/MultisiteOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineMarkerService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/SitecoreRenderingEngineBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ControllerEndpointExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/EncodingExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ForwardHeadersMarkerService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpContextExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpRequestExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RazorPageExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RequestLocalizationOptionsExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Filters/SitecoreLayoutContextControllerFilter.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreLayoutRequestMapper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingContext.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingEngineBuilder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Localization/SitecoreQueryStringCultureProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Mappers/SitecoreLayoutRequestMapper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/Site.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfo.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfoCollectionResult.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/MultisiteMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEngineMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEnginePipeline.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.Designer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.resx create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererFactory.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/EditableChromeRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRendererFactory.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IEditableChromeRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ModelBoundViewComponentComponentRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/RenderingHelpers.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/SitecoreRenderingContext.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/RenderingEngineConstants.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Routing/LanguageRouteConstraint.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteCollectionService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteResolver.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/SiteResolver.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/Sitecore.AspNetCore.SDK.RenderingEngine.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/ComponentHolder.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/FileTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/BindingViewComponent.cs create mode 100644 src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/SitecoreComponentViewComponent.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectInfo.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectType.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/Site.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfo.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfoResultModel.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SitemapOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Middleware/SitecoreRewriteMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Models/SitecoreRewriteOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRedirectRule.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRewriteRule.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/IRedirectsService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/ISitemapService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/SitemapService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.SearchOptimization/Sitecore.AspNetCore.SDK.SearchOptimization.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/DateTimeProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/IDateTimeProvider.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/SitecoreVisitorIdentificationOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/TagHelpers/SitecoreVisitorIdentificationTagHelper.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationConstants.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking/Sitecore.AspNetCore.SDK.Tracking.csproj create mode 100644 src/Sitecore.AspNetCore.SDK.Tracking/TrackingAppConfigurationExtensions.cs create mode 100644 src/icon.png create mode 100644 tests/Directory.Build.props create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/ISetupActionsProvider.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/SetupActionsProvider.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticMethodSetupActionsProvider.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticPropertySetupActionsProvider.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoNSubstituteDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoSetupDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoNSubstituteDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoSetupDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoNSubstituteDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoSetupDataAttribute.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Extensions/GuardClauseAssertionExtensions.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.AutoFixture/Sitecore.AspNetCore.SDK.AutoFixture.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Configuration/ExperienceEditorOptionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ServiceCollectionExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Mappers/SitecoreLayoutResponseMapperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Middleware/ExperienceEditorMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Models/ExperienceEditorPostModelFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataBuilderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataSerializerFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/EditFrameTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Sitecore.AspNetCore.SDK.GraphQL.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/ContextFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/DeviceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/EditableChromeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/MockModels/LinkFieldModel.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/HttpLayoutRequestHandlerOptionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutClientBuilderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestHandlerBuilderTClientFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestOptionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutServiceOptionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/DefaultLayoutClientFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/CouldNotContactSitecoreLayoutServiceClientExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/FieldReaderExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidRequestSitecoreLayoutServiceClientExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidResponseSitecoreLayoutServiceClientExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/ItemNotFoundSitecoreLayoutServiceClientExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceClientExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceMessageConfigurationExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceServerExceptionFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/DictionaryExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/HttpLayoutRequestHandlerBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/SitecoreLayoutRequestHttpExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/ServiceCollectionExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutRequestHandlerBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/TestAbstractSitecoreLayoutRequestHandler.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpLayoutRequestHandlerWrapper.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpMessageHandlerWrapper.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpResponseMessageWrapper.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/SitecoreLayoutRequestExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/SitecoreLayoutRequestFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/ComponentFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/ContextFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/EditableChromeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/FieldFixtureOfTField.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/FieldOfTValueFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Fields/CheckBoxFieldFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Fields/ContentListFieldFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Fields/ItemLinkFieldFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/FieldsReaderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/PlaceholderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/PlaceholderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Presentation/CachingDataFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Presentation/DeviceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Presentation/PersonalizationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Presentation/PlaceholderDataFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/Presentation/RenderingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/RouteFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/SimpleTestModel.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/SiteFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/Model/SitecoreDataFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/SitecoreLayoutResponseContentFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Response/SitecoreLayoutResponseFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldParserTests.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/PlaceholderFeatureConverterTests.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Fields/JsonSerializedFieldTests.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/JsonLayoutServiceSerializerFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/ExperienceEditorMiddlewareBenchmarks.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/Program.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/RenderingEngineBenchmarks.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/TrackingBenchmarks.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComplexComponent.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/Component2.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/Component3.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/Component4.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/Component5.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/Component6.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithAllFieldTypes.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithDates.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithFiles.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithImages.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithLinks.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithMissingData.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithNestedComponent.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithNumber.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/ComponentWithoutId.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomClass.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomContext.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomContextIndividual.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/CustomModelContextComponent.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/RouteFields.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/TestClass1.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ComponentModels/TestClass2.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/ControllerMiddlewareController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/ErrorController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/GlobalMiddlewareController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/HomeController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/MultisiteController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/TrackingController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/UsingAttributeController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/UsingGlobalMiddlewareController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/ComplexModelBindingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/CustomModelContextBindingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/ModelBindingErrorHandlingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/ModelBindingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Binding/ViewFieldsBindingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ControllerMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/CustomRenderTypes/LoggingComponentRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/CustomRenderTypes/MultipleComponentsAddedFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/CustomRenderTypes/PartialViewRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ErrorHandlingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ExperienceEditor/ExperienceEditorCustomRoutingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ExperienceEditor/ExperienceEditorFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ExperienceEditor/ExperienceEditorParsePathByItemIdFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/ForwardHeaders/ForwardHeadersToLayoutServiceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/GlobalMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/AdvanceLocalizationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/DefaultLocalizationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationUsingAttributeMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Mocks/CustomHttpClientFactory.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Multisite/MultisiteFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/RequestDefaultsConfigurationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/RequestExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/RequestHeadersValidationFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/RequestMappingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitecoreRewriteFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitemapProxyFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SitecoreLayoutClientBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/AllFieldTagHelpersFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/DateFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/FileFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/ImageFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/LinkFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/NumberFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/PlaceholderTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/RichTextFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/TextFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/AttributeBasedTrackingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/TrackingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/TrackingProxyFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/HttpLayoutClientMessageHandler.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLog.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLogger.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/IntegrationTestLoggerProvider.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Properties/launchSettings.json create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewComponents/Component1ViewComponent.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreContextViewModel.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreRouteViewModel.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreContext.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreResponse.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreRoute.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/Component1/Default.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComplexComponent.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component2.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component3.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.da.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component5.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component6.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithAllFieldTypes.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithDates.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithFIles.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithImages.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithLinks.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithMissingData.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithNumber.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithoutId.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomModelContextComponent.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Index.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/NestedPlaceholderPageLayout.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/RelativeUrls.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewForHandlingError.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewWithMissingComponent.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/VisitorIdentificationLayout.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_ComponentNotFound.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_PartialView.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/_ViewImports.cshtml create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.Development.json create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.json create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Attributes/UseSitecoreRenderingAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldsAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentParameterAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentPropertyAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextPropertyAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreLayoutResponseAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldsAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRoutePropertyAttributeFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/BindingViewComponentFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Extensions/BindingExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutComponentModelBinderProviderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutContextModelBinderProviderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutResponseModelBinderProviderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutRouteModelBinderProviderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreLayoutModelBinderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreViewModelBinderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldsBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentParameterBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentPropertyBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextPropertyBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutResponseBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldsBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRoutePropertyBindingSourceFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Configuration/RenderingEngineOptionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/EncodingExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpContextExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpRequestExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RazorPageExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ServiceCollectionExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Filters/SitecoreLayoutControllerFilterFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/MultisiteMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEngineMiddlewareFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEnginePipelineFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/BindingViewComponentMock.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ContentBlock.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/HeaderBlock.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomHtmlHelper.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomViewComponentHelper.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestBindingSource.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestComponentRenderer.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelBinderProviderContext.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelMetadata.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestPlaceholderFeature.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestSitecoreLayoutBindingSource.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/EditableChromeRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/LoggingComponentRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ModelBoundViewComponentComponentRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/PartialViewComponentRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/SitecoreRenderingContextFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Routing/LanguageRouteConstraintFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/SitecoreLayoutRequestMapperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/ComponentHolderFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/FileTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/ViewComponents/SitecoreComponentViewComponentFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/TrackingApplicationConfigurationExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/VisitorIdentificationConfigurationExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Sitecore.AspNetCore.SDK.Tracking.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.Tracking.Tests/TagHelpers/SitecoreVisitorIdentificationTagHelperFixture.cs create mode 100644 tests/Tests.GlobalSuppressions.cs create mode 100644 tests/data/Sitecore.AspNetCore.SDK.TestData/CannedResponses.cs create mode 100644 tests/data/Sitecore.AspNetCore.SDK.TestData/Models/CustomFieldType.cs create mode 100644 tests/data/Sitecore.AspNetCore.SDK.TestData/Serializer.cs create mode 100644 tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj create mode 100644 tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs create mode 100644 tests/data/json/devices.json create mode 100644 tests/data/json/edit-in-horizon-mode.json create mode 100644 tests/data/json/edit.json create mode 100644 tests/data/json/layoutResponse.json create mode 100644 tests/data/json/mixedComponentsEditChromes.json create mode 100644 tests/data/json/onlyComponents.json create mode 100644 tests/data/json/onlyEditChromes.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..588faf3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{yml,yaml}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml new file mode 100644 index 0000000..374d363 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -0,0 +1,42 @@ +name: Bug Report +description: File a bug report. +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + value: | + Description + + + Reproduction Steps + 1. Open + 2. Execute + + Actual Behavior + + Expected Behavior + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Feature Request.yml b/.github/ISSUE_TEMPLATE/Feature Request.yml new file mode 100644 index 0000000..ce003e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature Request.yml @@ -0,0 +1,24 @@ +name: Feature Request +description: Request a new feature. +labels: ["enhancement"] +body: + - type: textarea + id: feature + attributes: + label: What would you add to the SDK? + validations: + required: true + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this feature, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true + + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Question.yml b/.github/ISSUE_TEMPLATE/Question.yml new file mode 100644 index 0000000..491deba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.yml @@ -0,0 +1,18 @@ +name: Question +description: Ask any question about the project. +body: + - type: textarea + id: question + attributes: + label: What is your question? + validations: + required: true + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this question, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ef5fa01 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ + + + +## Description / Motivation + + + + + +## Testing + +- [ ] The Unit & Intergration tests are passing. +- [ ] I have added the necesary tests to cover my changes. + +## Terms + + + +- [ ] I agree to follow this project's [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/.github/workflows/Version.ps1 b/.github/workflows/Version.ps1 new file mode 100644 index 0000000..b3275bb --- /dev/null +++ b/.github/workflows/Version.ps1 @@ -0,0 +1,86 @@ +[CmdletBinding()] +Param ( + [Parameter(HelpMessage = "Path to the file to append the version elements in.")] + [string]$Path, + [Parameter(HelpMessage = "Previous version to calculate the next version on. Must be like '1.0.0'.")] + [string]$PreviousVersion, + [Parameter(HelpMessage = "Commit Message")] + [string]$Message +) + +function Get-Version +{ + param ( + [string]$PreviousVersion, + [string]$Message + ) + + $versionNumbers = $PreviousVersion -split "\." + $major = [int]$versionNumbers[0] + $minor = [int]$versionNumbers[1] + $patch = [int]$versionNumbers[2] + + if ($Env:GITHUB_REF -like "refs/pull/*/feat/*" -or $Message -like "FEAT: *") { + $minor++ + $patch = 0 + } elseif ($Env:GITHUB_REF -like "refs/pull/*/new/*" -or $Message -like "NEW: *") { + $major++ + $minor = 0 + $patch = 0 + } else { + $patch++ + } + + return "$major.$minor.$patch" +} + +function Get-VersionSuffix +{ + if ($Env:GITHUB_REF -like "refs/pull/*") { + $prId = $Env:GITHUB_REF -replace "refs/pull/(\d+)/.*", '$1' + $versionSuffix = "pr.$prId.$Env:GITHUB_RUN_NUMBER" + } else { + $versionSuffix = "" + } + + return $versionSuffix +} + +function Set-Version +{ + param ( + [string]$Path, + [string]$Version, + [string]$Suffix + ) + + $xml = [xml](Get-Content -Path $Path) + $properties = $xml.Project.PropertyGroup + + $assemblyVersionElement = $xml.CreateElement("AssemblyVersion") + $assemblyVersionElement.InnerText = "$Version.$Env:GITHUB_RUN_NUMBER" + + $versionElement = $xml.CreateElement("VersionPrefix") + $versionElement.InnerText = $Version + + $fileVersionElement = $xml.CreateElement("FileVersion") + $fileVersionElement.InnerText = "$Version.$Env:GITHUB_SHA" + + if (![string]::IsNullOrEmpty($Suffix)) { + $suffixElement = $xml.CreateElement("VersionSuffix") + $suffixElement.InnerText = $Suffix + $properties.AppendChild($suffixElement) + } + + $properties.AppendChild($assemblyVersionElement) + $properties.AppendChild($versionElement) + $properties.AppendChild($fileVersionElement) + + $xml.Save($Path) +} + +$newVersion = Get-Version $PreviousVersion $Message +$suffix = Get-VersionSuffix +Set-Version $Path $newVersion $suffix | Out-Null + +return "newVersion=$newVersion" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c8dfbb0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,114 @@ +name: Build + +on: + workflow_call: + inputs: + buildConfiguration: + type: string + required: true + description: 'The build configuration to use' + default: 'Release' + outputs: + newVersion: + description: 'The new version number' + value: ${{ jobs.build.outputs.newVersion }} + +jobs: + build: + runs-on: ubuntu-latest + outputs: + newVersion: ${{ steps.version.outputs.newVersion }} + + steps: + - uses: actions/checkout@v4 + + - name: Get Version Info + uses: actions/github-script@v7 + id: get-version-info + with: + script: | + async function getLatestRelease() { + try { + const response = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + core.info('Previous Release Version = ' + response.data.tag_name); + core.setOutput('previousVersion', response.data.tag_name); + } catch (error) { + if (error.status === 404) { + core.info('No releases found for this repository.'); + core.setOutput('previousVersion', '0.0.0'); + } else { + console.error('An error occurred while fetching the latest release: ', error); + throw error; + } + } + } + + async function getCommitMessage() { + try { + const response = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.sha + }); + core.info('Commit Message = ' + response.data.commit.message); + core.setOutput('commitMessage', response.data.commit.message); + } catch (error) { + console.error('An error occurred while fetching the commit message: ', error); + throw error; + } + } + + await getLatestRelease(); + await getCommitMessage(); + + - name: Version + id: version + shell: pwsh + run: | + $message = @" + ${{ steps.get-version-info.outputs.commitMessage }} + "@ + ./.github/workflows/Version.ps1 -Path "./src/Directory.Build.props" -PreviousVersion ${{ steps.get-version-info.outputs.previousVersion }} -Message $message >> $Env:GITHUB_OUTPUT + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c ${{ inputs.buildConfiguration }} --no-restore + + - name: Test + run: dotnet test -c ${{ inputs.buildConfiguration }} --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" --results-directory TestResults + + - name: Upload dotnet test results + uses: actions/upload-artifact@v4 + with: + name: test-results + path: TestResults + if: ${{ always() }} + + - name: Run Benchmarks + working-directory: ./tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks + run: dotnet run -c Release + + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: perf-results + path: ./tests/Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks/BenchmarkDotNet.Artifacts + + - name: Package + run: dotnet pack -c ${{ inputs.buildConfiguration }} --no-build --output nupkgs + + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: packages + path: nupkgs \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c6e7a88 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,85 @@ +name: CICD + +on: + push: + branches: [ "main" ] + +concurrency: + group: "cicd" + cancel-in-progress: false + +jobs: + build: + uses: ./.github/workflows/build.yml + with: + buildConfiguration: Release + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Create Release + id: create-release + uses: actions/github-script@v7 + with: + script: | + const newVersion = '${{ needs.build.outputs.newVersion }}'; + const response = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: newVersion, + target_commitish: context.sha, + name: newVersion, + generate_release_notes: true + }); + core.setOutput('upload_url', response.data.upload_url); + + - name: Download Artifact Packages + uses: actions/download-artifact@v4 + with: + name: packages + path: ./artifacts + + - name: Attach Release Assets + uses: actions/github-script@v7 + with: + script: | + const uploadUrl = '${{ steps.create-release.outputs.upload_url }}'; + const fs = require('fs'); + const path = require('path'); + const packages = fs.readdirSync('./artifacts').filter(file => file.endsWith('.nupkg')); + for (const file of packages) { + const filePath = path.join('./artifacts', file); + const name = path.basename(filePath); + const response = await github.rest.repos.uploadReleaseAsset({ + url: uploadUrl, + headers: { + 'content-type': 'application/zip', + 'content-length': fs.statSync(filePath).size + }, + name: name, + data: fs.readFileSync(filePath) + }); + core.info('Uploaded ' + name); + } + publish-docs: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Dotnet Setup + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.x + - run: dotnet tool update -g docfx + - run: docfx ./docfx/docfx.json + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './docfx/_site' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml new file mode 100644 index 0000000..442de12 --- /dev/null +++ b/.github/workflows/pullrequest.yml @@ -0,0 +1,11 @@ +name: PR + +on: + pull_request: + branches: [ "main" ] + +jobs: + build: + uses: ./.github/workflows/build.yml + with: + buildConfiguration: Release \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..20bcc4f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..ba368c8 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,42 @@ + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..45fa4cb --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Sitecore ASP.NET Core SDK + +This repository contains source code for all Sitecore ASP.NET Core SDK packages and templates to help you get started using the Sitecore ASP.NET Core SDK. + +## Getting started + +## Documentation and Community Resources + +- [Official Documentation](https://doc.sitecore.com/xp/en/developers/hd/latest/sitecore-headless-development/sitecore-asp-net-rendering-sdk.html) +- [StackExchange](https://sitecore.stackexchange.com/) + - Be sure to tag your question with the `aspnetcoresdk` tag. +- [Community Slack](https://sitecorechat.slack.com/messages/general) + - If you're not already a member of the Sitecore Community Slack, you can find more information here: https://siteco.re/sitecoreslack +- [Sitecore Community Forum](https://community.sitecore.com/community) + +## Contributions + +We are very grateful to the community for contributing bug fixes and improvements. We welcome all efforts to evolve and improve the Sitecore ASP.NET Core SDK; read below to learn how to participate in those efforts. + +### [Code of Conduct](CODE_OF_CONDUCT.md) + +Sitecore has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. + +### [Contributing Guide](CONTRIBUTING.md) + +Read our [contributing guide](CONTRIBUTING.md) to learn about our development process, how to propose bug fixes and improvements, and how to build and test your changes. + +### License + +The Sitecore ASP.NET Core SDK is using the [Apache 2.0 license](LICENSE.MD). + +## Support + +### Issues / Bugs / Feature Requests + +Open an issue via [GitHub](https://github.com/Sitecore/ASP.NET-Core-SDK/issues) + +Please use one of the provided templates when opening an issue, it will greatly increase your chances of a prompt response. + +Also, please try to refrain from asking "How to...?" questions via GitHub issues. If you have questions about how to use the SDK or implement something specific with the SDK, you'll likely find more success referring to the documentation, posting to Sitecore StackExchange, or chatting on Slack. diff --git a/Sitecore.AspNetCore.SDK.sln b/Sitecore.AspNetCore.SDK.sln new file mode 100644 index 0000000..5d2e34b --- /dev/null +++ b/Sitecore.AspNetCore.SDK.sln @@ -0,0 +1,223 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34701.34 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{03E05F99-1A3F-409C-8C8B-7DFE4265D56D}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Packages.props = Directory.Packages.props + nuget.config = nuget.config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0BE1019D-F812-4B03-9A6F-E3073A1CF0C9}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + src\GlobalSuppressions.cs = src\GlobalSuppressions.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Layout Service Client", "Layout Service Client", "{43F06962-0100-488C-8EF5-4735E85A545C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.LayoutService.Client", "src\Sitecore.AspNetCore.SDK.LayoutService.Client\Sitecore.AspNetCore.SDK.LayoutService.Client.csproj", "{B4833145-90B3-410E-9240-510B32E5FDA4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rendering Engine", "Rendering Engine", "{75482B5D-21E2-4DBE-BE78-657ECF0D409F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.GraphQL", "src\Sitecore.AspNetCore.SDK.GraphQL\Sitecore.AspNetCore.SDK.GraphQL.csproj", "{9A69EEE4-F7D2-4693-B557-E4D338F241C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.RenderingEngine", "src\Sitecore.AspNetCore.SDK.RenderingEngine\Sitecore.AspNetCore.SDK.RenderingEngine.csproj", "{EA632DA8-39FA-4181-8475-7D01FB5EA480}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.ExperienceEditor", "src\Sitecore.AspNetCore.SDK.ExperienceEditor\Sitecore.AspNetCore.SDK.ExperienceEditor.csproj", "{64B00F65-B625-47E3-BD4C-779556DEA018}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.SearchOptimization", "src\Sitecore.AspNetCore.SDK.SearchOptimization\Sitecore.AspNetCore.SDK.SearchOptimization.csproj", "{C0A69C38-9A77-4875-B3A9-9F170365D772}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.Tracking", "src\Sitecore.AspNetCore.SDK.Tracking\Sitecore.AspNetCore.SDK.Tracking.csproj", "{F19C565A-047C-4C91-AE2C-43687C9193FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification", "src\Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification\Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.csproj", "{109EAE14-6424-42F8-9877-0AB958A70E02}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{29DBF993-B6A4-4FA6-9CAA-730B319C164E}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + tests\Tests.GlobalSuppressions.cs = tests\Tests.GlobalSuppressions.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Layout Service Client", "Layout Service Client", "{61050ECA-956C-4BE1-8187-781603DC35C1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.AutoFixture", "tests\Sitecore.AspNetCore.SDK.AutoFixture\Sitecore.AspNetCore.SDK.AutoFixture.csproj", "{9B95B4AD-E26A-40A1-A159-C75FB53C0821}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{B47DBA4E-A9DA-4830-8EED-CFA0B798740C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "json", "json", "{C309DD88-CE7C-4E8B-A068-0D4BDF824A02}" + ProjectSection(SolutionItems) = preProject + tests\data\json\devices.json = tests\data\json\devices.json + tests\data\json\edit-in-horizon-mode.json = tests\data\json\edit-in-horizon-mode.json + tests\data\json\edit.json = tests\data\json\edit.json + tests\data\json\layoutResponse.json = tests\data\json\layoutResponse.json + tests\data\json\mixedComponentsEditChromes.json = tests\data\json\mixedComponentsEditChromes.json + tests\data\json\onlyComponents.json = tests\data\json\onlyComponents.json + tests\data\json\onlyEditChromes.json = tests\data\json\onlyEditChromes.json + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.LayoutService.Client.Tests", "tests\Sitecore.AspNetCore.SDK.LayoutService.Client.Tests\Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.csproj", "{161F477E-4963-45B2-A0AD-CB7DB9A445FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.TestData", "tests\data\Sitecore.AspNetCore.SDK.TestData\Sitecore.AspNetCore.SDK.TestData.csproj", "{3E62CA06-4823-412D-99B6-231B76C8CB71}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unit", "Unit", "{B75F7FED-DAA6-41DC-ACBA-2193B9E0A685}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Integration", "Integration", "{D301D535-E35D-49E7-ADD7-F45D4CF9604B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests", "tests\Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests\Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj", "{4FF52EAA-D14E-4BFB-939C-FB79A968E2AC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rendering Engine", "Rendering Engine", "{5E0267C1-E1B1-471A-951C-4AC894F870B8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Performance", "Performance", "{1B31C12C-5D18-4675-8378-FBD9EEEF3793}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks", "tests\Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks\Sitecore.AspNetCore.SDK.RenderingEngine.Benchmarks.csproj", "{C01864D0-AE4F-404C-BAF3-626974FC7290}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unit", "Unit", "{BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.RenderingEngine.Tests", "tests\Sitecore.AspNetCore.SDK.RenderingEngine.Tests\Sitecore.AspNetCore.SDK.RenderingEngine.Tests.csproj", "{C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Integration", "Integration", "{7C5D334A-FBCF-42E9-8E08-99C6894D9A4D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests", "tests\Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests\Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj", "{74FA9495-EBAA-4204-9D9A-4BDD025A637A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.ExperienceEditor.Tests", "tests\Sitecore.AspNetCore.SDK.ExperienceEditor.Tests\Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.csproj", "{68302AAF-A2BA-4B15-8D63-AE03C641D38A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.GraphQL.Tests", "tests\Sitecore.AspNetCore.SDK.GraphQL.Tests\Sitecore.AspNetCore.SDK.GraphQL.Tests.csproj", "{1229ED65-0C15-468B-A979-C41B52C68D65}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sitecore.AspNetCore.SDK.Tracking.Tests", "tests\Sitecore.AspNetCore.SDK.Tracking.Tests\Sitecore.AspNetCore.SDK.Tracking.Tests.csproj", "{100C07C6-C68D-469F-9F15-139CB48CB7F0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{5FE82369-DEF2-4136-B74F-6E86DB91050E}" + ProjectSection(SolutionItems) = preProject + .github\pull_request_template.md = .github\pull_request_template.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{1706E43D-AC19-4FBB-9BFB-18A8B195580A}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\main.yml = .github\workflows\main.yml + .github\workflows\pullrequest.yml = .github\workflows\pullrequest.yml + .github\workflows\Version.ps1 = .github\workflows\Version.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{24CCC156-046B-4600-9DB0-FC3269A18747}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE\Bug Report.yml = .github\ISSUE_TEMPLATE\Bug Report.yml + .github\ISSUE_TEMPLATE\Feature Request.yml = .github\ISSUE_TEMPLATE\Feature Request.yml + .github\ISSUE_TEMPLATE\Question.yml = .github\ISSUE_TEMPLATE\Question.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B4833145-90B3-410E-9240-510B32E5FDA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4833145-90B3-410E-9240-510B32E5FDA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4833145-90B3-410E-9240-510B32E5FDA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4833145-90B3-410E-9240-510B32E5FDA4}.Release|Any CPU.Build.0 = Release|Any CPU + {9A69EEE4-F7D2-4693-B557-E4D338F241C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A69EEE4-F7D2-4693-B557-E4D338F241C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A69EEE4-F7D2-4693-B557-E4D338F241C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A69EEE4-F7D2-4693-B557-E4D338F241C4}.Release|Any CPU.Build.0 = Release|Any CPU + {EA632DA8-39FA-4181-8475-7D01FB5EA480}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA632DA8-39FA-4181-8475-7D01FB5EA480}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA632DA8-39FA-4181-8475-7D01FB5EA480}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA632DA8-39FA-4181-8475-7D01FB5EA480}.Release|Any CPU.Build.0 = Release|Any CPU + {64B00F65-B625-47E3-BD4C-779556DEA018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64B00F65-B625-47E3-BD4C-779556DEA018}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64B00F65-B625-47E3-BD4C-779556DEA018}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64B00F65-B625-47E3-BD4C-779556DEA018}.Release|Any CPU.Build.0 = Release|Any CPU + {C0A69C38-9A77-4875-B3A9-9F170365D772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0A69C38-9A77-4875-B3A9-9F170365D772}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0A69C38-9A77-4875-B3A9-9F170365D772}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0A69C38-9A77-4875-B3A9-9F170365D772}.Release|Any CPU.Build.0 = Release|Any CPU + {F19C565A-047C-4C91-AE2C-43687C9193FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F19C565A-047C-4C91-AE2C-43687C9193FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F19C565A-047C-4C91-AE2C-43687C9193FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F19C565A-047C-4C91-AE2C-43687C9193FE}.Release|Any CPU.Build.0 = Release|Any CPU + {109EAE14-6424-42F8-9877-0AB958A70E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {109EAE14-6424-42F8-9877-0AB958A70E02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {109EAE14-6424-42F8-9877-0AB958A70E02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {109EAE14-6424-42F8-9877-0AB958A70E02}.Release|Any CPU.Build.0 = Release|Any CPU + {9B95B4AD-E26A-40A1-A159-C75FB53C0821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B95B4AD-E26A-40A1-A159-C75FB53C0821}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B95B4AD-E26A-40A1-A159-C75FB53C0821}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B95B4AD-E26A-40A1-A159-C75FB53C0821}.Release|Any CPU.Build.0 = Release|Any CPU + {161F477E-4963-45B2-A0AD-CB7DB9A445FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161F477E-4963-45B2-A0AD-CB7DB9A445FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161F477E-4963-45B2-A0AD-CB7DB9A445FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161F477E-4963-45B2-A0AD-CB7DB9A445FA}.Release|Any CPU.Build.0 = Release|Any CPU + {3E62CA06-4823-412D-99B6-231B76C8CB71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E62CA06-4823-412D-99B6-231B76C8CB71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E62CA06-4823-412D-99B6-231B76C8CB71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E62CA06-4823-412D-99B6-231B76C8CB71}.Release|Any CPU.Build.0 = Release|Any CPU + {4FF52EAA-D14E-4BFB-939C-FB79A968E2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FF52EAA-D14E-4BFB-939C-FB79A968E2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FF52EAA-D14E-4BFB-939C-FB79A968E2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FF52EAA-D14E-4BFB-939C-FB79A968E2AC}.Release|Any CPU.Build.0 = Release|Any CPU + {C01864D0-AE4F-404C-BAF3-626974FC7290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C01864D0-AE4F-404C-BAF3-626974FC7290}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C01864D0-AE4F-404C-BAF3-626974FC7290}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C01864D0-AE4F-404C-BAF3-626974FC7290}.Release|Any CPU.Build.0 = Release|Any CPU + {C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A}.Release|Any CPU.Build.0 = Release|Any CPU + {74FA9495-EBAA-4204-9D9A-4BDD025A637A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74FA9495-EBAA-4204-9D9A-4BDD025A637A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74FA9495-EBAA-4204-9D9A-4BDD025A637A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74FA9495-EBAA-4204-9D9A-4BDD025A637A}.Release|Any CPU.Build.0 = Release|Any CPU + {68302AAF-A2BA-4B15-8D63-AE03C641D38A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68302AAF-A2BA-4B15-8D63-AE03C641D38A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68302AAF-A2BA-4B15-8D63-AE03C641D38A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68302AAF-A2BA-4B15-8D63-AE03C641D38A}.Release|Any CPU.Build.0 = Release|Any CPU + {1229ED65-0C15-468B-A979-C41B52C68D65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1229ED65-0C15-468B-A979-C41B52C68D65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1229ED65-0C15-468B-A979-C41B52C68D65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1229ED65-0C15-468B-A979-C41B52C68D65}.Release|Any CPU.Build.0 = Release|Any CPU + {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {43F06962-0100-488C-8EF5-4735E85A545C} = {0BE1019D-F812-4B03-9A6F-E3073A1CF0C9} + {B4833145-90B3-410E-9240-510B32E5FDA4} = {43F06962-0100-488C-8EF5-4735E85A545C} + {75482B5D-21E2-4DBE-BE78-657ECF0D409F} = {0BE1019D-F812-4B03-9A6F-E3073A1CF0C9} + {9A69EEE4-F7D2-4693-B557-E4D338F241C4} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {EA632DA8-39FA-4181-8475-7D01FB5EA480} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {64B00F65-B625-47E3-BD4C-779556DEA018} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {C0A69C38-9A77-4875-B3A9-9F170365D772} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {F19C565A-047C-4C91-AE2C-43687C9193FE} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {109EAE14-6424-42F8-9877-0AB958A70E02} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {61050ECA-956C-4BE1-8187-781603DC35C1} = {29DBF993-B6A4-4FA6-9CAA-730B319C164E} + {9B95B4AD-E26A-40A1-A159-C75FB53C0821} = {29DBF993-B6A4-4FA6-9CAA-730B319C164E} + {B47DBA4E-A9DA-4830-8EED-CFA0B798740C} = {29DBF993-B6A4-4FA6-9CAA-730B319C164E} + {C309DD88-CE7C-4E8B-A068-0D4BDF824A02} = {B47DBA4E-A9DA-4830-8EED-CFA0B798740C} + {161F477E-4963-45B2-A0AD-CB7DB9A445FA} = {B75F7FED-DAA6-41DC-ACBA-2193B9E0A685} + {3E62CA06-4823-412D-99B6-231B76C8CB71} = {B47DBA4E-A9DA-4830-8EED-CFA0B798740C} + {B75F7FED-DAA6-41DC-ACBA-2193B9E0A685} = {61050ECA-956C-4BE1-8187-781603DC35C1} + {D301D535-E35D-49E7-ADD7-F45D4CF9604B} = {61050ECA-956C-4BE1-8187-781603DC35C1} + {4FF52EAA-D14E-4BFB-939C-FB79A968E2AC} = {D301D535-E35D-49E7-ADD7-F45D4CF9604B} + {5E0267C1-E1B1-471A-951C-4AC894F870B8} = {29DBF993-B6A4-4FA6-9CAA-730B319C164E} + {1B31C12C-5D18-4675-8378-FBD9EEEF3793} = {5E0267C1-E1B1-471A-951C-4AC894F870B8} + {C01864D0-AE4F-404C-BAF3-626974FC7290} = {1B31C12C-5D18-4675-8378-FBD9EEEF3793} + {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} = {5E0267C1-E1B1-471A-951C-4AC894F870B8} + {C47DB8DA-5534-4A74-ACA1-C1AC9D1FAB4A} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} + {7C5D334A-FBCF-42E9-8E08-99C6894D9A4D} = {5E0267C1-E1B1-471A-951C-4AC894F870B8} + {74FA9495-EBAA-4204-9D9A-4BDD025A637A} = {7C5D334A-FBCF-42E9-8E08-99C6894D9A4D} + {68302AAF-A2BA-4B15-8D63-AE03C641D38A} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} + {1229ED65-0C15-468B-A979-C41B52C68D65} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} + {100C07C6-C68D-469F-9F15-139CB48CB7F0} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} + {1706E43D-AC19-4FBB-9BFB-18A8B195580A} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} + {24CCC156-046B-4600-9DB0-FC3269A18747} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E4F7126-B772-42CB-8F90-93B221ED0A72} + EndGlobalSection +EndGlobal diff --git a/docfx/.gitignore b/docfx/.gitignore new file mode 100644 index 0000000..9780d95 --- /dev/null +++ b/docfx/.gitignore @@ -0,0 +1,3 @@ +# Ignore assets generated at build time +_site/ +api/ \ No newline at end of file diff --git a/docfx/README.md b/docfx/README.md new file mode 100644 index 0000000..fead9e0 --- /dev/null +++ b/docfx/README.md @@ -0,0 +1,25 @@ +# ASP.NET Core SDK - DocFX Documentation +The code in the `/docfx` folder is used to generate the documentation for the Sitecore ASP.NET Core SDK. The documentation is generated using [DocFX](https://dotnet.github.io/docfx/). + +## Building the documentation locally +You can build and run this documentation locally by running the following commands + +### First time setup +```dotnetcli +dotnet tool update -g docfx +``` + +### Subsequent runs +:warning: Ensure you are in the `/docfx` folder + +#### Run the documentation local server +```dotnetcli +docfx docfx.json --serve +``` + +You will then be able to access the documentation site at [http://localhost:8080](http://localhost:8080) + +#### Build the documentation +```dotnetcli +docfx docfx.json +``` \ No newline at end of file diff --git a/docfx/docfx.json b/docfx/docfx.json new file mode 100644 index 0000000..76b06bc --- /dev/null +++ b/docfx/docfx.json @@ -0,0 +1,49 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "**/*.csproj" + ] + } + ], + "dest": "api" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern", + "template" + ], + "globalMetadata": { + "_appName": "ASP.NET Core SDK", + "_appTitle": "ASP.NET Core SDK", + "_appLogoPath": "images/Sitecore-Icon.png", + "_appFaviconPath": "images/Sitecore-Icon.png", + "_enableSearch": true, + "_disableBreadcrumb": true, + "pdf": false + } + } +} \ No newline at end of file diff --git a/docfx/images/Sitecore-Icon.png b/docfx/images/Sitecore-Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3c2026da6f1f45a180ece0e27221afb370a64d5d GIT binary patch literal 4089 zcmai1`9Bkm|KAv9Q|6kRO>#9tuC&6;RcLb)Ii_+SIdUX3#*h%Xk2w7IOA3)W`cD_+yQLet8{_=PMa!X>tN01_1y7C(KN-R>z$2AHbZ) zcG;Nu?3lQNOdUf20I0x!00Ih%M2>^N5GxZ5pkYj6_1FR3GO#cJ0G_5p58XKc0Nw{? zSOc2~;CgZFZ2=SbkZU{EhZf$>v@zhXC85VSRHK23&VVea+ihTwid*Da;(6dVP@aIN z&M`@)R%!w1Z&g004-~A%G`>?FjQ704nBQs9vsJp%teJDOJL;d#15M)4m#ql2RQmr# ziaffb|Bw(Xr_B^Zpd!mGbBg9&-JTC(CE1#B?cW!^gb?lq@T?^^rWzHL5Md@Li!Y6_ z2kT&6Q+d#?3V2#BYou!NZmL?iq~7k$>xdjm0_^M-JZ6Gr;{a! zhhO3;6O(E6R}n7c<(6bc?P>l!-$+3L=i)4 zo#CWSc>&9UNDK|h@}Lb2JAR{n0^?Ij9A;93_6wDY&=zp33ec3QeVZftG;b!;f9ttVTW!;vCe}vDihQ#_ocl#Izb;z=ilIBOnjuF^6pH z?YgOygZuNfA%6X3RfiYA8}gXv#Eq0B>e;uc3taJYP76*>2*L4y6yWKIOLzvL%Z{x) zLf(v#q=V}?Q4%Rpex=L)i5h(lY|8nH5phePmQd8S#7&LLjjCjpJ5j3PY*xM7)(ZA$ zHK)$%Bw<;Oi1WQe_PIQE>GEo;9Y!$@lv+6#-NR5jH)OQ_o z8LKeCQ+s4;+-KN#wZVuL?t47&H#$|!3$8=Ks|#px1>aWaKIIYRGPL%J zBsBV*2pSa4MCOLkYM98%u7Pp=VDb0JjB~=5D>a~%-*yh!;8cN<#pJ-0SDoPeeZu*d zPQ+r__R)f?HYFSdmYnCR{F%s=7FjK2z7pTsrv}{FGKkBwy?yPpH0Fc@9REYj;z~Xn z^CP(?xPhQr`Gz^-F;R@Sqkq<=h6luX&7KgM6xxE#@%J9i0%(XQgG54+6XM!*lKP`g z+Z>m8#Y<_AMdc+&Ap?uxC-C9Kz220tr7w=Q`lb0k&Tghmo%%aB<4}&JE2ASL$}6f4 z5vR`xl7L?1?%P7p^DX-Dy79b3c`5<-+k#_8k24;5(QdhCh1vCbxQH8U(jR!7+WoT= zrg5vx<4k=YRWqpQsf$GKLiRF%E4dedo{q5(L&~#7#jMY1iBy(lhiOJqJZy*O86OpF zYj}5e%)$9a6B$YFF<$Gj-km2-2FV}>JMawl!$F?1Q!9IcP`PPxAQyY2N`BdeO|@`w z3Ac+BIt+bBCdh8tJ6}<D4=|TZ@@$n6l;yS0$C((Y9@*?Q^}+ke;w?%Dq17Z@27U07 zuBN@g?MxvB-@rzf!s~Zi?xR|Z(;S6x9xL6Jz!F1fS%6Q~x;JQfdoKo1@jZJ^GTx{T zqHqJ1Kw8WgXrWKX@Q)?AUD6SoG<^Xfol*erjdG?s2*EVB5=px{u@L0}W3`M$CGemO z>zo2*M6)1i9if{*YJC^uZSKO`Ya6Bq)Jb4YG06B?$&`?(eRrt!Ohtx4ldip*AR)~e z>Cjg|jCcsb!BDf(23#<2ALnv?)~R~f;C-@k__*rqh=+wfH(@{-w#*kwyz-(XnUz`T ztwh^(2t0H6xI!|ehhK|81wdrvvIJHDqiF+fMBK9I5ETLiyLO$-Xp$@wDK_f|B@%)@~XQW@-!@StrwQu|l) za!owZV+GdAau0`qO3h{BZ7azj5M2D)R3Do`65mnx;b9|J{LzinyR~JYj-&GB23xi@ zjofUGn~cnDxBQu=5p3yyHzV>%6cWoLNm^35F82pYjhhOm#nT^#SgRjs)G_TZ@4<&X zF3-Z)Vz{iM9RI4_$~r6XnT-4nn7-PlxYKpq6jAfD3baDI zM_{ZWon;H6)VrC(>0 zNXkRmGz&s*T;0`x)a~l(gTBGGf1nXR_>!PV_tVhM(^IA^y_O6Y6B#{X)(E4c1!Q6krst z)Q2@vSyub52a7u%TT`xx;|=1r*2c*eI%a0DbeEj9O!5Q{u&*>Mdlr{6?uqS5t3ET{ zY3gdi#q^L`2h(os6OTZIJHv0R^{CFqav$E~1r!fEf-O(gP_DbmhP*=LEphPJaZG6k zxi2=Q+T|;36|HJi$rC3wv14I)VJ8>Is#4d|BERa5x?9W<$8R^E$vz2;1U|6^=Bt#1 zyfdPq)T4Vif1l?Ex3L&~qx3MO@6H=9jmm>SuK1x&anYlTP8H$`bot+#p&!Is5pw>~ z6?58o>W=Mc3AgQ~y*^2Zo`%&nf{qLDa$~t!ieJ^`-(_B>cf8!8r?s=MVT^|!PJ3yT zFK?{gn@D^izoq&*YV$OUtrHXkklT&wTGQEL^)6o0AQ}anIaMncp0#4Ob&Af#$gm(UW@HJGp0GNtzE@z@{Rs5{ zf91{To2pus^OAeI;f%3%Dk-SxewJu*Vu0UGvM0@Tkp79&K-v24e3&_u2$Z-cT_)_9I=XuT{^e%eCZ!NKGFPPa4mS?jHcrEJyjg{t+8jki_+p3g)Ej0t+ zPg)C8SjeL5=3cej@S`r-)JKm$)A)5qK`O;}<&u!7aO-|?W^!hDZ__57_9@E}Bs-t( zZg~3jaQFzv@ZduXx^IY=7JjRC=5qriXQcV<^2b*W#YB?Z+$4H42Zv5?W_DEb*E0^v zZCR+OjZCPB4(8V#Mt)#R$wZ6PNQC;aY7w^c_m|PTQeXxA5yx-bFM8qdY)@9&>U*0% zKX1LA6`6N6MhESONXI*VKh-4r;|1-{l`#<34IY-!*x3N-BozmmR2+S@yoZ-&T$_om zTuum1m`-fYGY!-W%M5b!Dce~2Cs&QZ@kXnqkfp3h59!@%AIdeUDDC01ignp+rb z!a22x|2m3M{_v?a7)wWD$hY*+xQDl{?bx>_(Obd`H*kG zW%BZ8Bc^whK&-&$f!F{uoUe2+7^r7LX<#+z+(H5zVOo+OQdXYMZ5)2|^WAq6Oqbfm z9o!O0JJCtMv>i(%*r2d>!=cnrIhj`M2fyEGB}XTvdAB@A%nh9`5&e0bH$8Hij5m^u z%ka)aUbfenN5QvOC;vU0Kds*RX|mLuZEcZkaFENr?74+;_{RJaO*LNC{ea)_zcc&i ztxa%M`RC{5o(4tNr29IE;8roiRzWTG>Qkr7)ut2a{We%fcjJ}m{pJ9R_!;{oDv8%j_gI@Tr9B(+L3PZn_3m1|i(Hmn*VP!(; zt_7^gY$XJyLqZJFbUtPe<}&vn+*(j4OS4(Y=9~iV6XT$#0VnK4zO%yr7au9=7>s>Z#!#pF*fn*)pj9Q7R~o#+*J zqc-2#76a3dnCw;G#f7wd8g2BkW-^l!<#{KC(=rwB2;<~!J0F{=cl}#8la)WmB{=pR zYD_w0V;MM@eq_62yD1t5ml4fL+{xU)zCg< zU%n8=$BRD$^p>dlH|_DL%@Nv15rGjBH^YuK=Oz9)>9VRvQ{Y!I^yk3V>CoTldL1`< zNqX%$GPwTd=-YWEFGt=`9x>a$xZd~EwNrC!H_CotHQ1l7PgCkn!V$4bzL(M!)A%x~ z27vqdarR!|GyxcrP@y5ID`9d*$O-YIWT&)HR>I;GN2PaX#*eSkMe20>pE}EEF=a#= zoWGM;c!H8Cwpt6HfB?H-ACf6?oI1aXmKbUlFz5e?D|AB{EP=C3w XM*qC|*$!AZ{?q|xMwZwHj2r2HEyadm literal 0 HcmV?d00001 diff --git a/docfx/images/Sitecore-Logo.svg b/docfx/images/Sitecore-Logo.svg new file mode 100644 index 0000000..304902a --- /dev/null +++ b/docfx/images/Sitecore-Logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docfx/index.md b/docfx/index.md new file mode 100644 index 0000000..deae8f1 --- /dev/null +++ b/docfx/index.md @@ -0,0 +1,10 @@ +--- +_layout: landing +--- + +![Sitecore Logo](./images/Sitecore-Icon.png) + +# ASP.NET Core SDK - API Documentation +This documentation is for the Sitecore ASP.NET Core SDK. The SDK contains a set of libraries that enable you to render Sitecore content in ASP.NET Core. + +To see guides on how to leverage the SDK in your Sitecore XM/XP or Sitecore XM Cloud projects, you can refer to the [Sitecore Documentation Site](https://doc.sitecore.com). \ No newline at end of file diff --git a/docfx/overview/index.md b/docfx/overview/index.md new file mode 100644 index 0000000..a419ae7 --- /dev/null +++ b/docfx/overview/index.md @@ -0,0 +1,83 @@ +--- +_layout: landing +--- + +# Overview +The ASP.NET Core SDK is built to help developers leverage Sitecore Layout Data in their applications, to build layouts and hydrate components. + +## Data flow +The SDK enables ASP.NET Core Applications to connect to a Sitecore instance of XM/XP or XMC and retrieve Layout Data. The Layout Data is a JSON object that represents the structure of a page in Sitecore. The Layout Data is used to render the page in the application. + +### Basic Execution Sequence +The ASP.NET CoreSDK uses GraphQL to retrieve Layout Data in JSON format. When using Sitecore XM Cloud or Sitecore Experience Edge, the SDK connects to the Sitecore Experience Edge service to retrieve the Layout Data. +When working with Sitecore XM or Sitecore XP CD servers, the SDK connects to the Sitecore Layout Service to retrieve the Layout Data. + +Below you can see a basic sequence diagram of the execution flow, showing how the data flows between the browser, the ASP.NET Core Application, and the Experience Edge or Layout Service. + +```mermaid +sequenceDiagram + Browser->>ASP.NET Core Application: Page Request + ASP.NET Core Application-->>Experience Edge / Layout Service: GraphQL Request + Experience Edge / Layout Service-->>ASP.NET Core Application: Layout Data JSON + ASP.NET Core Application->>Browser: Page HTML +``` + +### Full Execution Sequence +The full execution sequence is more detailed and shows how the Layout Data is used to render the page in the application. The sequence diagram below shows the full execution flow, including the rendering of the page in the application. + +```mermaid +sequenceDiagram + actor User + participant Browser + box ASP.NET Core Application + participant App as Standard Middleware + participant Middleware as Rendering Engine Middleware + participant Rendering + participant Client as Layout Service Client + end + participant Sitecore as Experience Edge or Layout Service + + User->>Browser: Browse to URI + Browser-->>App: HTTP Request + activate App + App->>App: Resolve Controller + App->>App: Resolve Action + note right of App: Regular MVC Rendering happens if
there is no [UseSitecoreRendering]
attribute on the action + opt has [UseSitecoreRendering] attribute + App->>Middleware: Execute + deactivate App + activate Middleware + Middleware-->>Client: Layout Service Request + activate Client + Client-->>+Sitecore: GraphQL Request + Sitecore-->>-Client: GraphQL Response + Client-->>Middleware: Layout Service Response + deactivate Client + Middleware->>Middleware: Update HTTP Context + Middleware-->>Rendering: HTTP+Rendering Context + deactivate Middleware + activate Rendering + Rendering->>Rendering: Invoke Action + Rendering->>Rendering: Execute Razor View + Rendering->>Rendering: + loop Resolve Component + alt is Model Bound View + Rendering->>Rendering: Model Binding + Rendering->>Rendering: Execute Razor View + else is Custom View Component + Rendering->>Rendering: Model Binding + Rendering->>Rendering: Execute Razor View + else is Partial View + Rendering->>Rendering: Model Binding + Rendering->>Rendering: Execute Razor View + end + end + note right of Rendering: Executes recursively for each placeholder + Rendering-->>App: HTML + deactivate Rendering + activate App + end + App-->>Browser: HTTP Response + deactivate App + Browser->>User: Display Page +``` \ No newline at end of file diff --git a/docfx/template/public/main.css b/docfx/template/public/main.css new file mode 100644 index 0000000..b3d0d0a --- /dev/null +++ b/docfx/template/public/main.css @@ -0,0 +1,7 @@ +img#logo { + width: 50px +} + +article .logo { + margin-bottom: 25px; +} \ No newline at end of file diff --git a/docfx/toc.yml b/docfx/toc.yml new file mode 100644 index 0000000..0be4b20 --- /dev/null +++ b/docfx/toc.yml @@ -0,0 +1,6 @@ +- name: Home + href: index.md +- name: Overview + href: overview/index.md +- name: API + href: api/ \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..ae9403e --- /dev/null +++ b/nuget.config @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..be8681a --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,31 @@ + + + + + + net8.0 + enable + enable + true + $(NoWarn.Replace(';CS1591',''));CS7035 + + sc-ivanlieckens + Sitecore + https://github.com/Sitecore/ASP.NET-Core-Rendering-SDK + sitecore + Apache-2.0 + True + icon.png + + + + + GlobalSuppressions.cs + + + True + \ + + + + \ No newline at end of file diff --git a/src/GlobalSuppressions.cs b/src/GlobalSuppressions.cs new file mode 100644 index 0000000..188f7b1 --- /dev/null +++ b/src/GlobalSuppressions.cs @@ -0,0 +1,13 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Not required.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Type confusion should not occur.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "ReSharper ordering rules used.")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Underscores are used for private class variables.")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "Not required.")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No headers required.")] diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorMarkerService.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorMarkerService.cs new file mode 100644 index 0000000..36b68ea --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorMarkerService.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; + +/// +/// Marker service used to identify when experience editor services have been registered. +/// +internal class ExperienceEditorMarkerService; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorOptions.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorOptions.cs new file mode 100644 index 0000000..3303998 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Configuration/ExperienceEditorOptions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; + +/// +/// The options to configure the experience editor middleware. +/// +public class ExperienceEditorOptions +{ + /// + /// Gets or sets the endpoint that represent editing application URLs. + /// + public string Endpoint { get; set; } = "/jss-render"; + + /// + /// Gets or sets the action list to configure the handler for Experience Editor custom post requests. + /// + public ICollection> ItemMappings { get; set; } = []; + + /// + /// Gets or sets the Jss Editing Secret. + /// + public string JssEditingSecret { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/ExperienceEditorConstants.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/ExperienceEditorConstants.cs new file mode 100644 index 0000000..9325e4e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/ExperienceEditorConstants.cs @@ -0,0 +1,20 @@ +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor; + +/// +/// Various constants relevant to the Experience Editor. +/// +public static class ExperienceEditorConstants +{ + /// + /// Constants relevant to the Sitecore tag helpers. + /// + public static class SitecoreTagHelpers + { + /// + /// The HTML tag used by the tag helper. + /// + public const string EditFrameHtmlTag = "sc-edit-frame"; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorAppConfigurationExtensions.cs new file mode 100644 index 0000000..f162356 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorAppConfigurationExtensions.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Middleware; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Extensions; + +/// +/// Configuration helpers for ExperienceEditor functionality. +/// +public static class ExperienceEditorAppConfigurationExtensions +{ + /// + /// Registers the Sitecore Experience Editor middleware into the . + /// + /// The instance of the to extend. + /// The so that additional calls can be chained. + public static IApplicationBuilder UseSitecoreExperienceEditor(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + object? experienceEditorMarker = app.ApplicationServices.GetService(typeof(ExperienceEditorMarkerService)); + if (experienceEditorMarker != null) + { + app.UseMiddleware(); + } + + return app; + } + + /// + /// Adds the Sitecore Experience Editor support services to the . + /// + /// The to add services to. + /// Configures the options. + /// The so that additional calls can be chained. + public static ISitecoreRenderingEngineBuilder WithExperienceEditor(this ISitecoreRenderingEngineBuilder serviceBuilder, Action? options = null) + { + ArgumentNullException.ThrowIfNull(serviceBuilder); + + IServiceCollection services = serviceBuilder.Services; + if (services.Any(s => s.ServiceType == typeof(ExperienceEditorMarkerService))) + { + return serviceBuilder; + } + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + if (options != null) + { + services.Configure(options); + } + + return serviceBuilder; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorOptionsExtensions.cs new file mode 100644 index 0000000..3859da0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Extensions/ExperienceEditorOptionsExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Extensions; + +/// +/// Extensions to help configure . +/// +public static class ExperienceEditorOptionsExtensions +{ + /// + /// Adds a custom mapping action. + /// + /// The to configure. + /// The mapping action to configure . + /// The so that additional calls can be chained. + public static ExperienceEditorOptions MapToRequest(this ExperienceEditorOptions options, Action mapAction) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(mapAction); + + options.ItemMappings.Add(mapAction); + + return options; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Mappers/SitecoreLayoutResponseMapper.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Mappers/SitecoreLayoutResponseMapper.cs new file mode 100644 index 0000000..5c5e063 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Mappers/SitecoreLayoutResponseMapper.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Mappers; + +/// +/// Class that maps the layout response according to the options. +/// +internal class SitecoreLayoutResponseMapper +{ + private readonly ICollection> _handlers; + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + public SitecoreLayoutResponseMapper(IOptions options) + { + ArgumentNullException.ThrowIfNull(options); + _handlers = options.Value.ItemMappings; + } + + /// + /// Maps the route to a request path. + /// + /// Layout Response. + /// Sitecore Path. + /// Request data. + /// Path of the request. + public string? MapRoute(SitecoreLayoutResponseContent response, string scPath, HttpRequest request) + { + ArgumentNullException.ThrowIfNull(response); + ArgumentException.ThrowIfNullOrWhiteSpace(scPath); + ArgumentNullException.ThrowIfNull(request); + + if (_handlers.Count == 0) + { + return scPath; + } + + foreach (Action handler in _handlers) + { + handler.Invoke(response, scPath, request); + } + + return request.Path.Value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Middleware/ExperienceEditorMiddleware.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Middleware/ExperienceEditorMiddleware.cs new file mode 100644 index 0000000..e7aaed4 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Middleware/ExperienceEditorMiddleware.cs @@ -0,0 +1,219 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Mappers; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Models; +using Sitecore.AspNetCore.SDK.LayoutService.Client; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Middleware; + +/// +/// The Experience Editor middleware implementation that handles POST requests from the Sitecore Experience Editor +/// and wraps the response HTML in a JSON format. +/// +/// +/// Initializes a new instance of the class. +/// +/// The next middleware to call. +/// The experience editor configuration options. +/// A configured instance of . +/// The to use for logging. +public class ExperienceEditorMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutSerializer serializer, ILogger logger) +{ + private readonly ISitecoreLayoutSerializer _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next)); + private readonly ExperienceEditorOptions _options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly SitecoreLayoutResponseMapper _responseMapper = new(options); + + /// + /// The middleware Invoke method. + /// + /// The current . + /// A Task to support async calls. + public async Task Invoke(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + if (IsExperienceEditorRequest(httpContext.Request)) + { + ExperienceEditorPostModel? postModel = await TryParseContentFromRequestBodyAsync(httpContext).ConfigureAwait(false); + + if (postModel == null || !CheckJssEditingSecret(postModel, httpContext) || !TrySetHttpContextFeaturesForNextHandler(httpContext, postModel)) + { + return; + } + + Stream realResponseStream = httpContext.Response.Body; + try + { + MemoryStream tmpResponseBuffer = new(); + + httpContext.Response.Body = tmpResponseBuffer; + + await _next(httpContext).ConfigureAwait(false); + + tmpResponseBuffer.Position = 0; + string responseBody = await new StreamReader(tmpResponseBuffer).ReadToEndAsync().ConfigureAwait(false); + + await using StreamWriter realResponseWriter = new(realResponseStream); + await realResponseWriter.WriteAsync("{\"html\":").ConfigureAwait(false); + + string html = JsonSerializer.Serialize(responseBody); + await realResponseWriter.WriteAsync(html).ConfigureAwait(false); + await realResponseWriter.WriteAsync("}").ConfigureAwait(false); + } + finally + { + httpContext.Response.Body = realResponseStream; + } + } + else + { + await _next(httpContext).ConfigureAwait(false); + } + } + + private static async Task ParseContentFromRequestBodyAsync(HttpContext context) + { + using StreamReader reader = new(context.Request.Body); + string body = await reader.ReadToEndAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(body)) + { + throw new FormatException("Empty request body"); + } + + return JsonSerializer.Deserialize(body, JsonLayoutServiceSerializer.GetDefaultSerializerOptions()) ?? new ExperienceEditorPostModel(); + } + + private static string GetSitecoreItemPathFromRequestBody(ExperienceEditorPostModel postModel) + { + string? result = string.Empty; + if (JsonDocument.Parse(postModel.Args[1]).RootElement.TryGetProperty(LayoutServiceClientConstants.Serialization.SitecoreDataPropertyName, out JsonElement sitecore) + && sitecore.TryGetProperty(LayoutServiceClientConstants.Serialization.ContextPropertyName, out JsonElement context) + && context.TryGetProperty("itemPath", out JsonElement path)) + { + result = path.GetString(); + } + + if (string.IsNullOrWhiteSpace(result) + && JsonDocument.Parse(postModel.Args[2]).RootElement.TryGetProperty("httpContext", out JsonElement httpContext) + && httpContext.TryGetProperty("request", out JsonElement request) + && request.TryGetProperty("path", out path)) + { + // keep backwards compatibility in case people use an older JSS version that doesn't send the path in the context. + result = path.GetString(); + } + + return result ?? string.Empty; + } + + private bool IsExperienceEditorRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + return httpRequest.Method == HttpMethods.Post && httpRequest.Path.Value!.Equals(_options.Endpoint, StringComparison.InvariantCultureIgnoreCase); + } + + private SitecoreLayoutResponseContent GetSitecoreLayoutContentFromRequestBody(ExperienceEditorPostModel postModel) + { + return _serializer.Deserialize(postModel.Args[1]) ?? new SitecoreLayoutResponseContent(); + } + + private string GetMappedPath(string scPath, SitecoreLayoutResponseContent response, HttpRequest request) + { + string? customMappedPath = _responseMapper.MapRoute(response, scPath, request); + return string.IsNullOrWhiteSpace(customMappedPath) ? scPath : customMappedPath; + } + + private bool TrySetHttpContextFeaturesForNextHandler(HttpContext httpContext, ExperienceEditorPostModel postModel) + { + try + { + // parse POST body and set rendering context + SitecoreLayoutResponseContent content = GetSitecoreLayoutContentFromRequestBody(postModel); + + SitecoreRenderingContext scContext = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + httpContext.SetSitecoreRenderingContext(scContext); + + // Changing POST request to GET stream + httpContext.Request.Method = HttpMethods.Get; + + // Replace the request path with the item path from request metadata. + // This is needed to support different routing or other kinds of path dependent logic. + string scPath = GetSitecoreItemPathFromRequestBody(postModel); + if (!string.IsNullOrWhiteSpace(scPath)) + { + httpContext.Request.Path = GetMappedPath(scPath, content, httpContext.Request); + } + else + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + _logger.LogError("Error parsing Layout content from POST request: Empty Item path."); + return false; + } + } + + // Disabled catching general exceptions because all exceptions in request parsing shall be treated as bad requests. + catch (Exception exception) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + _logger.LogError(exception, "Error parsing Layout content from POST request"); + return false; + } + + return true; + } + + private bool CheckJssEditingSecret(ExperienceEditorPostModel postModel, HttpContext httpContext) + { + string localSecret = _options.JssEditingSecret; + if (string.IsNullOrEmpty(localSecret)) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + _logger.LogError("The JSS_EDITING_SECRET environment variable is missing or invalid."); + return false; + } + + string? secretFromRequest = postModel.JssEditingSecret; + + bool result = localSecret == secretFromRequest; + if (!result) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + _logger.LogError("Missing or invalid secret"); + } + + return result; + } + + private async Task TryParseContentFromRequestBodyAsync(HttpContext httpContext) + { + ExperienceEditorPostModel postModel; + try + { + postModel = await ParseContentFromRequestBodyAsync(httpContext).ConfigureAwait(false); + } + catch (Exception exception) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + _logger.LogError(exception, "Error parsing POST request"); + return null; + } + + return postModel; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Models/ExperienceEditorPostModel.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Models/ExperienceEditorPostModel.cs new file mode 100644 index 0000000..c1038ab --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Models/ExperienceEditorPostModel.cs @@ -0,0 +1,53 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Models; + +/// +/// Represents the model used to store the experience editor post. +/// +public class ExperienceEditorPostModel +{ + /// + /// Gets or sets the Id which has the name of the JSS app in the content tree/configuration. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the function name. + /// + public string? FunctionName { get; set; } + + /// + /// Gets or sets the module name. + /// + public string? ModuleName { get; set; } + + /// + /// Gets or sets the Args which is an array that contains JSON strings. + /// + /// + /// By default, the array has the following structure: + /// + /// { + /// request path, + /// serialized data { + /// sitecore (a root property) { + /// context (contains additional details, like language, site, user and the item path), + /// route (contains the item's properties and layout details) } }, + /// serialized view bag { + /// language, + /// dictionary (localization), + /// httpContext { + /// request { + /// url, + /// path, + /// querystring (a key-value dictionary ), + /// userAgent } } } } + /// + /// The item path is the path that would be seen, when navigating to it on frontend, i.e. it is a site relative link. + /// + public List Args { get; set; } = []; + + /// + /// Gets or sets the Jss Editing Secret. + /// + public string? JssEditingSecret { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..1cdd454 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sitecore.AspNetCore.SDK.ExperienceEditor.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to ViewContext parameter cannot be null.. + /// + internal static string Exception_ViewContextCannotBeNull { + get { + return ResourceManager.GetString("Exception_ViewContextCannotBeNull", resourceCulture); + } + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.resx b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.resx new file mode 100644 index 0000000..53d3fe2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Properties/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ViewContext parameter cannot be null. + + \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Sitecore.AspNetCore.SDK.ExperienceEditor.csproj b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Sitecore.AspNetCore.SDK.ExperienceEditor.csproj new file mode 100644 index 0000000..e4cebdd --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/Sitecore.AspNetCore.SDK.ExperienceEditor.csproj @@ -0,0 +1,33 @@ + + + + Sitecore Editing Host + .NET SDK for creating a Sitecore Headless Editing Host supporting Experience Editor + + + + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.ExperienceEditor.Tests + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataBuilder.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataBuilder.cs new file mode 100644 index 0000000..e20c7ae --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataBuilder.cs @@ -0,0 +1,120 @@ +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +/// +internal class ChromeDataBuilder : IChromeDataBuilder +{ + /// + public ChromeCommand MapButtonToCommand(EditButtonBase button, string? itemId, IDictionary? parameters) + { + if (button is DividerEditButton dividerEditButton) + { + return new ChromeCommand + { + Click = "chrome:dummy", + Header = dividerEditButton.Header, + Icon = dividerEditButton.Icon, + IsDivider = true, + Tooltip = null, + Type = "separator" + }; + } + + if (button is WebEditButton webEditButton && !string.IsNullOrWhiteSpace(webEditButton.Click)) + { + return CommandBuilder(webEditButton, itemId, parameters); + } + + FieldEditButton? fieldEditButton = button as FieldEditButton; + string fieldsString = string.Join('|', fieldEditButton?.Fields ?? []); + webEditButton = new WebEditButton + { + Click = $"webedit:fieldeditor(command={DefaultEditFrameButtonIds.Edit},fields={fieldsString})", + Tooltip = button.Tooltip, + Header = button.Header, + Icon = button.Icon, + }; + + return CommandBuilder(webEditButton, itemId, parameters); + } + + private static ChromeCommand CommandBuilder(WebEditButton button, string? itemId, IDictionary? frameParameters) + { + if (string.IsNullOrWhiteSpace(button.Click) || + button.Click.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase) || + button.Click.StartsWith("chrome:", StringComparison.OrdinalIgnoreCase) || + string.IsNullOrWhiteSpace(itemId)) + { + return new ChromeCommand + { + IsDivider = false, + Type = button.Type, + Header = button.Header ?? string.Empty, + Icon = button.Icon ?? string.Empty, + Tooltip = button.Tooltip ?? string.Empty, + Click = button.Click ?? string.Empty, + }; + } + + string? message = button.Click; + Dictionary parameters = []; + + // Extract any parameters already in the command + int length = button.Click.IndexOf('(', StringComparison.OrdinalIgnoreCase); + if (length >= 0) + { + int end = button.Click.IndexOf(')', StringComparison.OrdinalIgnoreCase); + if (end < 0) + { + throw new ArgumentException("Message with arguments must end with )."); + } + + parameters = button.Click[(length + 1)..end] + .Split(',') + .Select(x => x.Trim()) + .Aggregate(new Dictionary(), (previous, current) => + { + string[] parts = current.Split('='); + previous[parts[0]] = parts.Length < 2 + ? string.Empty + : parts[1]; + return previous; + }); + + message = button.Click[..length]; + } + + parameters["id"] = itemId; + + if (button.Parameters != null && button.Parameters.Any()) + { + foreach ((string key, object? value) in button.Parameters) + { + parameters[key] = value?.ToString() ?? string.Empty; + } + } + + if (frameParameters != null && frameParameters.Any()) + { + foreach ((string key, object? value) in frameParameters) + { + parameters[key] = value?.ToString() ?? string.Empty; + } + } + + string parameterString = string.Join(", ", parameters.Select(x => $"{x.Key}={x.Value}")); + string click = $"{message}({parameterString})"; + + return new ChromeCommand + { + IsDivider = false, + Type = button.Type, + Header = button.Header ?? string.Empty, + Icon = button.Icon ?? string.Empty, + Tooltip = button.Tooltip ?? string.Empty, + Click = $"javascript:Sitecore.PageModes.PageEditor.postRequest('{click}',null,false)" + }; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataSerializer.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataSerializer.cs new file mode 100644 index 0000000..ee2cc08 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/ChromeDataSerializer.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +/// +internal class ChromeDataSerializer : IChromeDataSerializer +{ + private static readonly JsonSerializerOptions DefaultSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + public string Serialize(Dictionary chromeData) + { + return JsonSerializer.Serialize(chromeData, DefaultSerializerOptions); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/EditFrameTagHelper.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/EditFrameTagHelper.cs new file mode 100644 index 0000000..33e164d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/EditFrameTagHelper.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Properties; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +/// +/// Tag helper for the Sitecore placeholder element. +/// +/// +/// Initializes a new instance of the class. +/// +/// An instance of . +/// An instance of . +[HtmlTargetElement(ExperienceEditorConstants.SitecoreTagHelpers.EditFrameHtmlTag)] +public class EditFrameTagHelper(IChromeDataBuilder chromeDataBuilder, IChromeDataSerializer chromeDataSerializer) + : TagHelper +{ + private readonly IChromeDataBuilder _chromeDataBuilder = chromeDataBuilder ?? throw new ArgumentNullException(nameof(chromeDataBuilder)); + private readonly IChromeDataSerializer _chromeDataSerializer = chromeDataSerializer ?? throw new ArgumentNullException(nameof(chromeDataSerializer)); + + /// + /// Gets or sets the current view context for the tag helper. + /// + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext? ViewContext { get; set; } + + /// + /// Gets or sets the title of edit frame. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the tooltip of edit frame. + /// + public string? Tooltip { get; set; } + + /// + /// Gets or sets the CSS class which be applied for edit frame. + /// + public string? CssClass { get; set; } + + /// + /// Gets or sets the collection of edit frame buttons. + /// + public IEnumerable? Buttons { get; set; } + + /// + /// Gets or sets the data source of edit frame. + /// + public EditFrameDataSource? Source { get; set; } + + /// + /// Gets or sets the parameters of edit frame. + /// + public IDictionary? Parameters { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + SitecoreData? sitecoreData = GetSitecoreData(); + output.TagName = string.Empty; + + if (sitecoreData?.Context is not { IsEditing: true }) + { + return; + } + + Dictionary chromeData = []; + Dictionary frameProps = []; + + if (Source != null) + { + string? databaseName = Source.DatabaseName ?? sitecoreData.Route?.DatabaseName; + string language = Source.Language ?? sitecoreData.Context.Language; + + chromeData["contextItemUri"] = frameProps["sc_item"] = $"sitecore://{databaseName}/{Source.ItemId}?lang={language}"; + } + + frameProps["class"] = $"scLooseFrameZone {CssClass}".Trim(); + chromeData["displayName"] = Title; + chromeData["expandedDisplayName"] = Tooltip; + chromeData["commands"] = Buttons?.Select(btn => _chromeDataBuilder.MapButtonToCommand(btn, Source?.ItemId, Parameters)).ToList(); + + TagBuilder chromeDataTagBuilder = new("span"); + chromeDataTagBuilder.AddCssClass("scChromeData"); + chromeDataTagBuilder.InnerHtml.Append(_chromeDataSerializer.Serialize(chromeData)); + + TagBuilder frameZoneTagBuilder = new("div"); + foreach ((string key, string? value) in frameProps) + { + frameZoneTagBuilder.Attributes.Add(key, value); + } + + frameZoneTagBuilder.InnerHtml.AppendHtml(chromeDataTagBuilder); + + TagHelperContent? innerContent = await output.GetChildContentAsync().ConfigureAwait(false); + frameZoneTagBuilder.InnerHtml.AppendHtml(innerContent.GetContent()); + + output.Content.SetHtmlContent(frameZoneTagBuilder); + } + + private SitecoreData? GetSitecoreData() + { + if (ViewContext == null) + { + throw new NullReferenceException(Resources.Exception_ViewContextCannotBeNull); + } + + ISitecoreRenderingContext? renderingContext = ViewContext.HttpContext.GetSitecoreRenderingContext(); + return renderingContext?.Response?.Content?.Sitecore; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataBuilder.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataBuilder.cs new file mode 100644 index 0000000..372e349 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataBuilder.cs @@ -0,0 +1,18 @@ +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +/// +/// Contract for configuring Chrome Data clients. +/// +public interface IChromeDataBuilder +{ + /// + /// Maps object to . + /// + /// The edit button to build a ChromeCommand. + /// The ID of the item the EditFrame is associated with. + /// Additional parameters passed to the EditFrame. + /// Instance of . + ChromeCommand MapButtonToCommand(EditButtonBase button, string? itemId, IDictionary? parameters); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataSerializer.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataSerializer.cs new file mode 100644 index 0000000..ea33093 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/IChromeDataSerializer.cs @@ -0,0 +1,14 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; + +/// +/// Contract that supports serialization for the Chrome Data. +/// +public interface IChromeDataSerializer +{ + /// + /// Serializes the given data to the string in JSON format. + /// + /// The data for serialization. + /// The JSON string. + string Serialize(Dictionary chromeData); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/ChromeCommand.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/ChromeCommand.cs new file mode 100644 index 0000000..fb9709f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/ChromeCommand.cs @@ -0,0 +1,37 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class contains fields for a command in the chrome data. +/// +public class ChromeCommand +{ + /// + /// Gets or sets a value indicating whether is it divider or not. + /// + public bool IsDivider { get; set; } + + /// + /// Gets or sets the type of command. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the header of command. + /// + public string Header { get; set; } = default!; + + /// + /// Gets or sets the icon path of command. + /// + public string Icon { get; set; } = default!; + + /// + /// Gets or sets the tooltip of command. + /// + public string? Tooltip { get; set; } + + /// + /// Gets or sets the click action of command. + /// + public string Click { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButton.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButton.cs new file mode 100644 index 0000000..d8b1fc5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButton.cs @@ -0,0 +1,84 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class contains a set of default edit frame buttons. +/// +public static class DefaultEditFrameButton +{ + /// + /// Gets edit layout button. + /// + public static WebEditButton EditLayout => new() + { + Header = "Edit Layout", + Icon = "/~/icon/Office/16x16/document_selection.png", + Click = "webedit:openexperienceeditor", + Tooltip = "Open the item for editing", + }; + + /// + /// Gets delete button. + /// + public static WebEditButton Delete => new() + { + Header = "Delete Link", + Icon = "/~/icon/Office/16x16/delete.png", + Click = "webedit:delete", + Tooltip = "Delete the item", + }; + + /// + /// Gets move up button. + /// + public static WebEditButton MoveUp => new() + { + Header = "Move Up", + Icon = "/~/icon/Office/16x16/navigate_up.png", + Click = "item:moveup", + Tooltip = "Move the item up", + }; + + /// + /// Gets move down button. + /// + public static WebEditButton MoveDown => new() + { + Header = "Move Down", + Icon = "/~/icon/Office/16x16/navigate_down.png", + Click = "item:movedown", + Tooltip = "Move the item down", + }; + + /// + /// Gets move first button. + /// + public static WebEditButton MoveFirst => new() + { + Header = "Move First", + Icon = "/~/icon/Office/16x16/navigate_up2.png", + Click = "item:movefirst", + Tooltip = "Move the item first", + }; + + /// + /// Gets move last button. + /// + public static WebEditButton MoveLast => new() + { + Header = "Move Last", + Icon = "/~/icon/Office/16x16/navigate_down2.png", + Click = "item:movelast", + Tooltip = "Move the item last", + }; + + /// + /// Gets insert button. + /// + public static WebEditButton Insert => new() + { + Header = "Insert New", + Icon = "/~/icon/Office/16x16/insert_from_template.png", + Click = "webedit:new", + Tooltip = "Insert a new item", + }; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButtonIds.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButtonIds.cs new file mode 100644 index 0000000..38e91a3 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DefaultEditFrameButtonIds.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// Various constants relevant to the Edit Frame. +/// +public static class DefaultEditFrameButtonIds +{ + /// + /// Gets the ID of Edit item (/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default/Edit). + /// + public static readonly string Edit = "{70C4EED5-D4CD-4D7D-9763-80C42504F5E7}"; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DividerEditButton.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DividerEditButton.cs new file mode 100644 index 0000000..7271b99 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/DividerEditButton.cs @@ -0,0 +1,13 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class represents a button-separator for edit frame. +/// +public class DividerEditButton : EditButtonBase +{ + /// + public override string Header => "Separator"; + + /// + public override string Icon => string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditButtonBase.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditButtonBase.cs new file mode 100644 index 0000000..095c4d5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditButtonBase.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class contains base fields for edit buttons. +/// +public abstract class EditButtonBase +{ + /// + /// Gets or sets the title of the button. + /// + public virtual string? Header { get; set; } + + /// + /// Gets or sets the icon path. + /// + public virtual string? Icon { get; set; } + + /// + /// Gets or sets the tooltip of the button. + /// + public virtual string? Tooltip { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditFrameDataSource.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditFrameDataSource.cs new file mode 100644 index 0000000..26a634a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/EditFrameDataSource.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class represents the data source for Edit Frame. +/// +public class EditFrameDataSource +{ + /// + /// Gets or sets the item ID. + /// + public string? ItemId { get; set; } + + /// + /// Gets or sets the database name. + /// + public string? DatabaseName { get; set; } + + /// + /// Gets or sets the language. + /// + public string? Language { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/FieldEditButton.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/FieldEditButton.cs new file mode 100644 index 0000000..fa56e99 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/FieldEditButton.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class represents the edit button which allows manipulation with defined fields. +/// +public class FieldEditButton : EditButtonBase +{ + /// + /// Gets the collection of the field names. + /// + public IEnumerable Fields { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/WebEditButton.cs b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/WebEditButton.cs new file mode 100644 index 0000000..3211ad9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.ExperienceEditor/TagHelpers/Model/WebEditButton.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; + +/// +/// This class represents web edit button. +/// +public class WebEditButton : EditButtonBase +{ + /// + /// Gets or sets the click action of the button. + /// + public string? Click { get; set; } + + /// + /// Gets or sets the type of button. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the additional parameters of the button. + /// + public IDictionary? Parameters { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs new file mode 100644 index 0000000..da87e38 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs @@ -0,0 +1,26 @@ +using GraphQL.Client.Abstractions.Websocket; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; + +namespace Sitecore.AspNetCore.SDK.GraphQL.Client.Models; + +/// +/// GraphQL Client options needed for Preview or Edge schemas. +/// +public class SitecoreGraphQlClientOptions : GraphQLHttpClientOptions +{ + /// + /// Gets or sets ApiKey. + /// + public string? ApiKey { get; set; } + + /// + /// Gets or sets Default site name, used by middlewares which use GraphQl client. + /// + public string? DefaultSiteName { get; set; } + + /// + /// Gets or sets GraphQLJsonSerializer, which could be SystemTextJsonSerializer or NewtonsoftJsonSerializer, SystemTextJsonSerializer by default. + /// + public IGraphQLWebsocketJsonSerializer GraphQlJsonSerializer { get; set; } = new SystemTextJsonSerializer(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs new file mode 100644 index 0000000..0cd0d78 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs @@ -0,0 +1,33 @@ +namespace Sitecore.AspNetCore.SDK.GraphQL.Exceptions; + +/// +/// Details an exception that may occur during GraphQl configuration. +/// +public class InvalidGraphQlConfigurationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidGraphQlConfigurationException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public InvalidGraphQlConfigurationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public InvalidGraphQlConfigurationException(string message, Exception innerException) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs new file mode 100644 index 0000000..31923da --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs @@ -0,0 +1,57 @@ +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.GraphQL.Client.Models; +using Sitecore.AspNetCore.SDK.GraphQL.Exceptions; + +namespace Sitecore.AspNetCore.SDK.GraphQL.Extensions; + +/// +/// Sitemap configuration. +/// +public static class GraphQlConfigurationExtensions +{ + /// + /// Configuration for GraphQLClient. + /// + /// The to add services to. + /// The configuration for GraphQL client. + /// The so that additional calls can be chained. + public static IServiceCollection AddGraphQlClient(this IServiceCollection services, Action configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.Configure(configuration); + + SitecoreGraphQlClientOptions graphQlClientOptions = TryGetConfiguration(configuration); + + services.AddSingleton(_ => + { + GraphQLHttpClient graphQlHttpClient = new(graphQlClientOptions.EndPoint!, graphQlClientOptions.GraphQlJsonSerializer); + + graphQlHttpClient.HttpClient.DefaultRequestHeaders.Add("sc_apikey", graphQlClientOptions.ApiKey); + return graphQlHttpClient; + }); + + return services; + } + + private static SitecoreGraphQlClientOptions TryGetConfiguration(Action configuration) + { + SitecoreGraphQlClientOptions graphQlClientOptions = new(); + configuration.Invoke(graphQlClientOptions); + + if (string.IsNullOrWhiteSpace(graphQlClientOptions.ApiKey)) + { + throw new InvalidGraphQlConfigurationException("Empty ApiKey, provided in GraphQLClientOptions."); + } + + if (graphQlClientOptions.EndPoint == null) + { + throw new InvalidGraphQlConfigurationException("Empty EndPoint, provided in GraphQLClientOptions."); + } + + return graphQlClientOptions; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Sitecore.AspNetCore.SDK.GraphQL.csproj b/src/Sitecore.AspNetCore.SDK.GraphQL/Sitecore.AspNetCore.SDK.GraphQL.csproj new file mode 100644 index 0000000..1270519 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Sitecore.AspNetCore.SDK.GraphQL.csproj @@ -0,0 +1,14 @@ + + + + Sitecore GQL + .NET Client for Sitecore GQL + + + + + + + + + diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/HttpLayoutRequestHandlerOptions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/HttpLayoutRequestHandlerOptions.cs new file mode 100644 index 0000000..fa1d46b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/HttpLayoutRequestHandlerOptions.cs @@ -0,0 +1,14 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// Options to control the for the Sitecore layout service. +/// +public class HttpLayoutRequestHandlerOptions : IMapRequest +{ + /// + public List> RequestMap { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientBuilder.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientBuilder.cs new file mode 100644 index 0000000..7c07ee6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// +/// Initializes a new instance of the class. +/// +/// The initial . +public class SitecoreLayoutClientBuilder(IServiceCollection services) + : ISitecoreLayoutClientBuilder +{ + /// + public IServiceCollection Services { get; protected set; } = services ?? throw new ArgumentNullException(nameof(services)); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientOptions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientOptions.cs new file mode 100644 index 0000000..18cf2a2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutClientOptions.cs @@ -0,0 +1,19 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// Options to control the Sitecore . +/// +public class SitecoreLayoutClientOptions +{ + /// + /// Gets the registry of Sitecore layout service request handlers. + /// + public Dictionary> HandlerRegistry { get; init; } = []; + + /// + /// Gets or sets the default handler name for requests. + /// + public string? DefaultHandler { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestHandlerBuilder.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestHandlerBuilder.cs new file mode 100644 index 0000000..e6b59b7 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestHandlerBuilder.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// +/// Initializes a new instance of the class. +/// +public class SitecoreLayoutRequestHandlerBuilder : ILayoutRequestHandlerBuilder + where THandler : ILayoutRequestHandler +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the handler being configured. + /// The initial . + public SitecoreLayoutRequestHandlerBuilder(string handlerName, IServiceCollection services) + { + ArgumentException.ThrowIfNullOrWhiteSpace(handlerName); + ArgumentNullException.ThrowIfNull(services); + + Services = services; + HandlerName = handlerName; + } + + /// + public IServiceCollection Services { get; } + + /// + public string HandlerName { get; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestOptions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestOptions.cs new file mode 100644 index 0000000..2c1661d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutRequestOptions.cs @@ -0,0 +1,20 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// Options to control the . +/// +public class SitecoreLayoutRequestOptions +{ + private SitecoreLayoutRequest _requestDefaults = []; + + /// + /// Gets or sets the default parameters for all requests made using the . + /// + public SitecoreLayoutRequest RequestDefaults + { + get => _requestDefaults; + set => _requestDefaults = value ?? throw new ArgumentNullException(nameof(value)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutServiceMarkerService.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutServiceMarkerService.cs new file mode 100644 index 0000000..69643fb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Configuration/SitecoreLayoutServiceMarkerService.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; + +/// +/// Marker service used to identify when Sitecore layout service services have been registered. +/// +internal class SitecoreLayoutServiceMarkerService; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/DefaultLayoutClient.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/DefaultLayoutClient.cs new file mode 100644 index 0000000..e3da3ad --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/DefaultLayoutClient.cs @@ -0,0 +1,113 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client; + +/// +/// +/// Initializes a new instance of the class. +/// +/// The services used for handler resolution. +/// The for this instance. +/// An to access specific options for the default client request. +/// The to use for logging. +public class DefaultLayoutClient( + IServiceProvider services, + IOptions layoutClientOptions, + IOptionsSnapshot layoutRequestOptions, + ILogger logger) + : ISitecoreLayoutClient +{ + private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services)); + private readonly IOptions _layoutClientOptions = layoutClientOptions ?? throw new ArgumentNullException(nameof(layoutClientOptions)); + private readonly IOptionsSnapshot _layoutRequestOptions = layoutRequestOptions ?? throw new ArgumentNullException(nameof(layoutRequestOptions)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + public async Task Request(SitecoreLayoutRequest request) + { + ArgumentNullException.ThrowIfNull(request); + return await Request(request, string.Empty).ConfigureAwait(false); + } + + /// + public async Task Request(SitecoreLayoutRequest request, string handlerName) + { + ArgumentNullException.ThrowIfNull(request); + + string? finalHandlerName = !string.IsNullOrWhiteSpace(handlerName) ? handlerName : _layoutClientOptions.Value.DefaultHandler; + + if (string.IsNullOrWhiteSpace(finalHandlerName)) + { + throw new ArgumentNullException(finalHandlerName, Resources.Exception_HandlerNameIsNull); + } + + if (!_layoutClientOptions.Value.HandlerRegistry.TryGetValue(finalHandlerName, out Func? value)) + { + throw new KeyNotFoundException(string.Format(Resources.Exception_HandlerRegistryKeyNotFound, finalHandlerName)); + } + + SitecoreLayoutRequestOptions mergedLayoutRequestOptions = MergeLayoutRequestOptions(finalHandlerName); + + SitecoreLayoutRequest finalRequest = request.UpdateRequest(mergedLayoutRequestOptions.RequestDefaults); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + string serializedRequest = JsonSerializer.Serialize(finalRequest); + _logger.LogTrace("Sitecore Layout Request {serializedRequest}", serializedRequest); + } + + ILayoutRequestHandler handler = value.Invoke(_services); + + return await handler.Request(finalRequest, finalHandlerName).ConfigureAwait(false); + } + + private static bool AreEqual(SitecoreLayoutRequest request1, SitecoreLayoutRequest request2) + { + if (request1.Count != request2.Count) + { + return false; + } + + ICollection dictionary1Keys = request1.Keys; + foreach (string key in dictionary1Keys) + { + if (!(request2.TryGetValue(key, out object? value) && + request1[key] == value)) + { + return false; + } + } + + return true; + } + + private SitecoreLayoutRequestOptions MergeLayoutRequestOptions(string handlerName) + { + SitecoreLayoutRequestOptions globalLayoutRequestOptions = _layoutRequestOptions.Value; + SitecoreLayoutRequestOptions handlerLayoutRequestOptions = _layoutRequestOptions.Get(handlerName); + + if (AreEqual(globalLayoutRequestOptions.RequestDefaults, handlerLayoutRequestOptions.RequestDefaults)) + { + return globalLayoutRequestOptions; + } + + SitecoreLayoutRequest mergedRequestDefaults = globalLayoutRequestOptions.RequestDefaults; + SitecoreLayoutRequest handlerRequestDefaults = handlerLayoutRequestOptions.RequestDefaults; + + foreach (KeyValuePair entry in handlerRequestDefaults) + { + mergedRequestDefaults[entry.Key] = handlerRequestDefaults[entry.Key]; + } + + globalLayoutRequestOptions.RequestDefaults = mergedRequestDefaults; + + return globalLayoutRequestOptions; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/CouldNotContactSitecoreLayoutServiceClientException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/CouldNotContactSitecoreLayoutServiceClientException.cs new file mode 100644 index 0000000..d1b5adc --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/CouldNotContactSitecoreLayoutServiceClientException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when the Sitecore layout service cannot be contacted. +/// +public class CouldNotContactSitecoreLayoutServiceClientException : SitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public CouldNotContactSitecoreLayoutServiceClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public CouldNotContactSitecoreLayoutServiceClientException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public CouldNotContactSitecoreLayoutServiceClientException() + : base(Resources.Exception_CouldNotContactService) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public CouldNotContactSitecoreLayoutServiceClientException(Exception innerException) + : this(Resources.Exception_CouldNotContactService, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/FieldReaderException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/FieldReaderException.cs new file mode 100644 index 0000000..ea385cd --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/FieldReaderException.cs @@ -0,0 +1,47 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when reading a Field. +/// +public class FieldReaderException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public FieldReaderException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public FieldReaderException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type attempting to be read. + public FieldReaderException(Type type) + : base(string.Format(Resources.Exception_ReadingField, type)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type attempting to be read. + /// The inner exception to be wrapped. + public FieldReaderException(Type type, Exception innerException) + : base(string.Format(Resources.Exception_ReadingField, type), innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidRequestSitecoreLayoutServiceClientException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidRequestSitecoreLayoutServiceClientException.cs new file mode 100644 index 0000000..4b62b45 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidRequestSitecoreLayoutServiceClientException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when the Sitecore layout service is invoked with an invalid request. +/// +public class InvalidRequestSitecoreLayoutServiceClientException : SitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public InvalidRequestSitecoreLayoutServiceClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public InvalidRequestSitecoreLayoutServiceClientException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public InvalidRequestSitecoreLayoutServiceClientException() + : base(Resources.Exception_InvalidRequestError) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public InvalidRequestSitecoreLayoutServiceClientException(Exception innerException) + : this(Resources.Exception_InvalidRequestError, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidResponseSitecoreLayoutServiceClientException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidResponseSitecoreLayoutServiceClientException.cs new file mode 100644 index 0000000..15c8ea9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/InvalidResponseSitecoreLayoutServiceClientException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when the Sitecore layout service returns an invalid response. +/// +public class InvalidResponseSitecoreLayoutServiceClientException : SitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public InvalidResponseSitecoreLayoutServiceClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public InvalidResponseSitecoreLayoutServiceClientException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public InvalidResponseSitecoreLayoutServiceClientException() + : base(Resources.Exception_InvalidResponseFormat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public InvalidResponseSitecoreLayoutServiceClientException(Exception innerException) + : this(Resources.Exception_InvalidResponseFormat, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/ItemNotFoundSitecoreLayoutServiceClientException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/ItemNotFoundSitecoreLayoutServiceClientException.cs new file mode 100644 index 0000000..e362126 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/ItemNotFoundSitecoreLayoutServiceClientException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when the Sitecore layout service returns a 'not found' (404) response. +/// +public class ItemNotFoundSitecoreLayoutServiceClientException : SitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public ItemNotFoundSitecoreLayoutServiceClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public ItemNotFoundSitecoreLayoutServiceClientException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ItemNotFoundSitecoreLayoutServiceClientException() + : base(Resources.Exception_ItemNotFoundError) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public ItemNotFoundSitecoreLayoutServiceClientException(Exception innerException) + : this(Resources.Exception_ItemNotFoundError, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceClientException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceClientException.cs new file mode 100644 index 0000000..fea3333 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceClientException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when communicating with the Sitecore layout service. +/// +public class SitecoreLayoutServiceClientException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public SitecoreLayoutServiceClientException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public SitecoreLayoutServiceClientException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutServiceClientException() + : this(Resources.Exception_GeneralServiceError) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public SitecoreLayoutServiceClientException(Exception innerException) + : this(Resources.Exception_GeneralServiceError, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceMessageConfigurationException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceMessageConfigurationException.cs new file mode 100644 index 0000000..3cdfba0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceMessageConfigurationException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when invalid configuration is applied to the message sent to the Sitecore layout service. +/// +public class SitecoreLayoutServiceMessageConfigurationException : SitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public SitecoreLayoutServiceMessageConfigurationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public SitecoreLayoutServiceMessageConfigurationException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutServiceMessageConfigurationException() + : base(Resources.Exception_MessageConfigurationError) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public SitecoreLayoutServiceMessageConfigurationException(Exception innerException) + : this(Resources.Exception_MessageConfigurationError, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceServerException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceServerException.cs new file mode 100644 index 0000000..64bd0f8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Exceptions/SitecoreLayoutServiceServerException.cs @@ -0,0 +1,45 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +/// +/// Details an exception that may occur when the Sitecore layout service returns a server related error. +/// +public class SitecoreLayoutServiceServerException : InvalidResponseSitecoreLayoutServiceClientException +{ + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + public SitecoreLayoutServiceServerException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// The inner exception to be wrapped. + public SitecoreLayoutServiceServerException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutServiceServerException() + : base(Resources.Exception_LayoutServiceServerError) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception to be wrapped. + public SitecoreLayoutServiceServerException(Exception innerException) + : this(Resources.Exception_LayoutServiceServerError, innerException) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/DictionaryExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..5418d9e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/DictionaryExtensions.cs @@ -0,0 +1,20 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// Extension methods to convert dictionary collection to string format. +/// +internal static class DictionaryExtensions +{ + /// + /// Converts dictionary collection to string format. + /// + /// The key of the dictionary. + /// The value of the dictionary. + /// The dictionary being configured. + /// The configured . + public static string ToDebugString(this IDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + return "{" + string.Join(",", dictionary.Select(kv => kv.Key + "=" + kv.Value).ToArray()) + "}"; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/JsonSerializerOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/JsonSerializerOptionsExtensions.cs new file mode 100644 index 0000000..4671ed4 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// Extension methods for . +/// +public static class JsonSerializerOptionsExtensions +{ + /// + /// Adds the default Layout Service serialization settings to the provided . + /// + /// The to add the default settings to. + /// The modified with the default settings added. + public static JsonSerializerOptions AddLayoutServiceDefaults(this JsonSerializerOptions options) + { + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString; + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + options.PropertyNameCaseInsensitive = true; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new FieldConverter()); + options.Converters.Add(new PlaceholderFeatureConverter(new FieldParser())); + + return options; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/LayoutRequestHandlerBuilderExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/LayoutRequestHandlerBuilderExtensions.cs new file mode 100644 index 0000000..79ee059 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/LayoutRequestHandlerBuilderExtensions.cs @@ -0,0 +1,110 @@ +using System.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// Extension methods to support configuration of Sitecore layout request handler services. +/// +public static class LayoutRequestHandlerBuilderExtensions +{ + /// + /// Sets the current handler being built as the default handler for Sitecore layout service client requests. + /// + /// The type of handler being configured. + /// The builder being configured. + /// The configured . + public static ILayoutRequestHandlerBuilder AsDefaultHandler( + this ILayoutRequestHandlerBuilder builder) + where THandler : ILayoutRequestHandler + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.Configure(options => options.DefaultHandler = builder.HandlerName); + + return builder; + } + + /// + /// Registers the default Sitecore layout service request options for the given handler. + /// + /// The type of handler being configured. + /// The being configured. + /// The request options configuration. + /// The configured . + public static ILayoutRequestHandlerBuilder WithRequestOptions( + this ILayoutRequestHandlerBuilder builder, + Action configureRequest) + where THandler : ILayoutRequestHandler + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureRequest); + + builder.Services.Configure(builder.HandlerName, options => configureRequest(options.RequestDefaults)); + + return builder; + } + + /// + /// Registers a configuration action as named for the given handler. + /// + /// The to configure. + /// The configuration based on . + /// The configured . + public static ILayoutRequestHandlerBuilder MapFromRequest( + this ILayoutRequestHandlerBuilder builder, Action configureHttpRequestMessage) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureHttpRequestMessage); + + builder.Services.Configure(builder.HandlerName, options => options.RequestMap.Add(configureHttpRequestMessage)); + + return builder; + } + + /// + /// Adds default configuration for the HTTP request message. + /// + /// The to configure. + /// The list of headers which should not be validated. + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder ConfigureRequest( + this ILayoutRequestHandlerBuilder httpHandlerBuilder, string[] nonValidatedHeaders) + { + ArgumentNullException.ThrowIfNull(httpHandlerBuilder); + ArgumentNullException.ThrowIfNull(nonValidatedHeaders); + + httpHandlerBuilder.MapFromRequest((request, message) => + { + message.RequestUri = message.RequestUri != null + ? request.BuildDefaultSitecoreLayoutRequestUri(message.RequestUri) + : null; + + if (request.TryReadValue(RequestKeys.AuthHeaderKey, out string? headerValue)) + { + message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", headerValue); + } + + if (request.TryGetHeadersCollection(out Dictionary? headers)) + { + foreach (KeyValuePair h in headers ?? []) + { + if (nonValidatedHeaders.Contains(h.Key)) + { + message.Headers.TryAddWithoutValidation(h.Key, h.Value); + } + else + { + message.Headers.Add(h.Key, h.Value); + } + } + } + }); + + return httpHandlerBuilder; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/ServiceCollectionExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..8fb38ab --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// Extension methods to support Microsoft.Extensions.DependencyInjection. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the services required to support the Sitecore layout service to the given . + /// + /// The to add services to. + /// >An action to configure the . + /// An so that Sitecore layout services may be configured further. + public static ISitecoreLayoutClientBuilder AddSitecoreLayoutService( + this IServiceCollection services, + Action? options = null) + { + ArgumentNullException.ThrowIfNull(services); + + // Only register services if marker interface is missing + if (services.All(s => s.ServiceType != typeof(SitecoreLayoutServiceMarkerService))) + { + services.AddTransient( + sp => + { + using IServiceScope scope = sp.CreateScope(); + return ActivatorUtilities.CreateInstance(scope.ServiceProvider, sp); + }); + + SetSerializer(services); + } + + if (options != null) + { + services.Configure(options); + } + + return new SitecoreLayoutClientBuilder(services); + } + + private static void SetSerializer(IServiceCollection services) + { + services.AddSingleton(new JsonLayoutServiceSerializer()); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs new file mode 100644 index 0000000..7aef363 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs @@ -0,0 +1,313 @@ +using System.Text.Json.Serialization; +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// Extension methods to support configuration of layout service services. +/// +public static class SitecoreLayoutClientBuilderExtensions +{ + /// + /// Registers a handler of type to handle requests. + /// + /// The type of service to be registered for this . + /// The being configured. + /// The name used to identify the handler. + /// Optional factory to control the instantiation of the client. + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHandler( + this ISitecoreLayoutClientBuilder builder, + string name, + Func? factory = null) + where THandler : ILayoutRequestHandler + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + Type handlerType = typeof(THandler); + if (handlerType.IsInterface) + { + throw new ArgumentException(string.Format(Resources.Exception_RegisterTypesOfService, typeof(THandler))); + } + + if (handlerType.IsAbstract && factory == null) + { + throw new ArgumentException(Resources.Exception_AbstractRegistrationsMustProvideFactory); + } + + factory ??= sp => ActivatorUtilities.CreateInstance(sp); + + builder.Services.Configure(options => + { + options.HandlerRegistry[name] = sp => + { + using IServiceScope scope = sp.CreateScope(); + return factory(scope.ServiceProvider); + }; + }); + + return new SitecoreLayoutRequestHandlerBuilder(name, builder.Services); + } + + /// + /// Registers a graphQl handler to handle requests. + /// + /// The being configured. + /// The name used to identify the handler. + /// The siteName used to identify the handler. + /// The apiKey to access graphQl endpoint. + /// GraphQl endpoint uri. + /// Default language for GraphQl requests. + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddGraphQlHandler( + this ISitecoreLayoutClientBuilder builder, + string name, + string siteName, + string apiKey, + Uri uri, + string defaultLanguage = "en") + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(siteName); + ArgumentNullException.ThrowIfNull(apiKey); + ArgumentNullException.ThrowIfNull(uri); + + GraphQLHttpClient client = new(uri, new SystemTextJsonSerializer()); + client.HttpClient.DefaultRequestHeaders.Add("sc_apikey", apiKey); + + builder.WithDefaultRequestOptions(request => + { + request + .SiteName(siteName) + .ApiKey(apiKey); + if (!request.ContainsKey(RequestKeys.Language)) + { + request.Language(defaultLanguage); + } + }); + return builder.AddHandler(name, (sp) + => ActivatorUtilities.CreateInstance( + sp, client, sp.GetRequiredService(), sp.GetRequiredService>())); + } + + /// + /// Registers a graphQl handler to handle requests, it uses already configured GraphQL client. + /// + /// The being configured. + /// The name used to identify the handler. + /// The siteName used to identify the handler. + /// Default language for GraphQl requests. + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddGraphQlHandler( + this ISitecoreLayoutClientBuilder builder, + string name, + string siteName, + string defaultLanguage = "en") + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(siteName); + + builder.WithDefaultRequestOptions(request => + { + request + .SiteName(siteName); + if (!request.ContainsKey(RequestKeys.Language)) + { + request.Language(defaultLanguage); + } + }); + return builder.AddHandler(name, sp + => ActivatorUtilities.CreateInstance( + sp, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); + } + + /// + /// Registers the default layout service request options for all handlers. + /// + /// The being configured. + /// The request options configuration. + /// The configured . + public static ISitecoreLayoutClientBuilder WithDefaultRequestOptions(this ISitecoreLayoutClientBuilder builder, Action configureRequest) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureRequest); + + builder.Services.ConfigureAll(options => configureRequest(options.RequestDefaults)); + + return builder; + } + + /// + /// Configures System.Text.Json specific features such as input and output formatters. + /// + /// The being configured. + /// The so that additional calls can be chained. + public static ISitecoreLayoutClientBuilder AddSystemTextJson(this ISitecoreLayoutClientBuilder builder) + { + ServiceDescriptor descriptor = new(typeof(ISitecoreLayoutSerializer), typeof(JsonLayoutServiceSerializer), ServiceLifetime.Singleton); + builder.Services.Replace(descriptor); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Registers a HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// A function to resolve the to be used. Be aware, that the underlying associated to the HttpClient will be reused across multiple sessions. + /// To prevent data, leaking among sessions, make sure Cookies are not cached. See for reference https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#cookies . + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + Func resolveClient) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(resolveClient); + + ILayoutRequestHandlerBuilder httpHandlerBuilder = builder.AddHandler(handlerName, sp => + { + HttpClient client = resolveClient(sp); + return ActivatorUtilities.CreateInstance(sp, client); + }); + + httpHandlerBuilder.ConfigureRequest([]); + + return httpHandlerBuilder; + } + + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// A function to resolve the to be used. Be aware, that the underlying associated to the HttpClient will be reused across multiple sessions. + /// To prevent data, leaking among sessions, make sure Cookies are not cached. See for reference https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#cookies . + /// The list of headers which should not be validated. + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + Func resolveClient, + string[] nonValidatedHeaders) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(resolveClient); + ArgumentNullException.ThrowIfNull(nonValidatedHeaders); + + ILayoutRequestHandlerBuilder httpHandlerBuilder = builder.AddHandler(handlerName, sp => + { + HttpClient client = resolveClient(sp); + return ActivatorUtilities.CreateInstance(sp, client); + }); + + httpHandlerBuilder.ConfigureRequest(nonValidatedHeaders); + + return httpHandlerBuilder; + } + + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// An action to configure the . + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(configure); + + builder.Services.AddHttpClient(handlerName, configure); + + return AddHttpHandler(builder, handlerName, sp => sp.GetRequiredService().CreateClient(handlerName)); + } + + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// An action to configure the . + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(configure); + + builder.Services.AddHttpClient(handlerName, configure).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + // DO NOT REMOVE - this prevents from cookies being shared among private sessions. + UseCookies = false + }); + + return AddHttpHandler(builder, handlerName, sp => sp.GetRequiredService().CreateClient(handlerName)); + } + + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// The used for the . + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + Uri uri) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(uri); + + return AddHttpHandler(builder, handlerName, client => client.BaseAddress = uri); + } + + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The name of the request handler being registered. + /// The used for the . + /// The so that additional calls can be chained. + public static ILayoutRequestHandlerBuilder AddHttpHandler( + this ISitecoreLayoutClientBuilder builder, + string handlerName, + string uri) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(handlerName); + ArgumentNullException.ThrowIfNull(uri); + + return AddHttpHandler(builder, handlerName, new Uri(uri)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutRequestExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutRequestExtensions.cs new file mode 100644 index 0000000..144a94d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutRequestExtensions.cs @@ -0,0 +1,85 @@ +using System.Net; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; + +/// +/// HTTP related extension methods for the . +/// +internal static class SitecoreLayoutRequestExtensions +{ + private static readonly List DefaultSitecoreRequestKeys = + [ + RequestKeys.SiteName, + RequestKeys.Path, + RequestKeys.Language, + RequestKeys.ApiKey, + RequestKeys.Mode, + RequestKeys.PreviewDate + ]; + + /// + /// Build a URI using the default Sitecore layout entries in the provided request. + /// + /// The request object. + /// The base URI used to compose the final URI. + /// A URI containing the base URI and the relevant entries in the request object added as query strings. + public static Uri BuildDefaultSitecoreLayoutRequestUri(this SitecoreLayoutRequest request, Uri baseUri) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(baseUri); + + return request.BuildUri(baseUri, DefaultSitecoreRequestKeys); + } + + /// + /// Build a URI using the default Sitecore layout entries in the provided request. + /// + /// The request object. + /// The base URI used to compose the final URI. + /// The additional URI query parameters to get from the request. + /// A URI containing the base URI and the relevant entries in the request object added as query strings. + public static Uri BuildDefaultSitecoreLayoutRequestUri(this SitecoreLayoutRequest request, Uri baseUri, IEnumerable additionalQueryParameters) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(baseUri); + ArgumentNullException.ThrowIfNull(additionalQueryParameters); + + List defaultKeys = [.. DefaultSitecoreRequestKeys]; + defaultKeys.AddRange(additionalQueryParameters); + + return request.BuildUri(baseUri, defaultKeys); + } + + /// + /// Build a URI using all the entries in the provided request. + /// + /// The request object. + /// The base URI used to compose the final URL. + /// The URI query parameters to get from request. + /// A URI containing the base URI and all the valid entries in the request object added as query strings. + public static Uri BuildUri(this SitecoreLayoutRequest request, Uri baseUri, IEnumerable queryParameters) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(baseUri); + ArgumentNullException.ThrowIfNull(queryParameters); + + List> entries = request.Where(entry => queryParameters.Contains(entry.Key)).ToList(); + IEnumerable> validQueryParts = entries.Where(entry => entry.Value is string && !string.IsNullOrWhiteSpace(entry.Value.ToString()))!; + string[] queryParts = validQueryParts.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value.ToString())}").ToArray(); + + if (queryParts.Length == 0) + { + return baseUri; + } + + string queryString = $"?{string.Join("&", queryParts)}"; + + UriBuilder builder = new(baseUri) + { + Query = queryString + }; + + return builder.Uri; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandler.cs new file mode 100644 index 0000000..0e78317 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandler.cs @@ -0,0 +1,18 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +/// +/// Supports making requests to the Sitecore layout service. +/// +public interface ILayoutRequestHandler +{ + /// + /// Handles a request to the Sitecore layout service using the specified handler. + /// + /// The request details. + /// The name of the request handler to use to handle the request. + /// The response of the request. + Task Request(SitecoreLayoutRequest request, string handlerName); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandlerBuilder.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandlerBuilder.cs new file mode 100644 index 0000000..ad8a18c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ILayoutRequestHandlerBuilder.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +/// +/// Contract for configuring named Sitecore layout service request handlers. +/// +/// The type of handler being configured. +public interface ILayoutRequestHandlerBuilder + where THandler : ILayoutRequestHandler +{ + /// + /// Gets the where Sitecore layout services are configured. + /// + IServiceCollection Services { get; } + + /// + /// Gets the name of the handler being configured. + /// + string HandlerName { get; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/IMapRequest.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/IMapRequest.cs new file mode 100644 index 0000000..89f8898 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/IMapRequest.cs @@ -0,0 +1,16 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +/// +/// Contract for mapping entries to an object of the given type. +/// +/// The type the request is mapped to. +public interface IMapRequest + where T : class +{ + /// + /// Gets the list of mappings from a to T. + /// + List> RequestMap { get; init; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClient.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClient.cs new file mode 100644 index 0000000..8f4e282 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClient.cs @@ -0,0 +1,17 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +/// +/// Supports making requests to the Sitecore layout service. +/// +public interface ISitecoreLayoutClient : ILayoutRequestHandler +{ + /// + /// Invokes a request to the Sitecore layout service using the default handler name. + /// + /// The request details. + /// The response of the request. + Task Request(SitecoreLayoutRequest request); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClientBuilder.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClientBuilder.cs new file mode 100644 index 0000000..1d5765c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Interfaces/ISitecoreLayoutClientBuilder.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; + +/// +/// Contract for configuring Sitecore layout service clients. +/// +public interface ISitecoreLayoutClientBuilder +{ + /// + /// Gets the where Sitecore layout services are configured. + /// + IServiceCollection Services { get; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/LayoutServiceConstants.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/LayoutServiceConstants.cs new file mode 100644 index 0000000..eb73e0b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/LayoutServiceConstants.cs @@ -0,0 +1,45 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client; + +/// +/// Constants of the Layout Service Client. +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Multi layered constants for easy use.")] +public static class LayoutServiceClientConstants +{ + /// + /// Constants relevant to Sitecore layout service response chromes. + /// + public static class SitecoreChromes + { + /// + /// The name of the chrome type attribute. + /// + public const string ChromeTypeName = "type"; + + /// + /// The value of the chrome type attribute. + /// + public const string ChromeTypeValue = "text/sitecore"; + + /// + /// The default chrome HTML tag. + /// + public const string ChromeTag = "code"; + } + + /// + /// Constants relevant to Serialization. + /// + public static class Serialization + { + /// + /// The name of the SitecoreData property. + /// + public const string SitecoreDataPropertyName = "sitecore"; + + /// + /// The name of the Context property. + /// + public const string ContextPropertyName = "context"; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.Designer.cs new file mode 100644 index 0000000..a9ac577 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.Designer.cs @@ -0,0 +1,207 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sitecore.AspNetCore.SDK.LayoutService.Client.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Abstract registrations must provide a factory to resolve a layout service.. + /// + internal static string Exception_AbstractRegistrationsMustProvideFactory { + get { + return ResourceManager.GetString("Exception_AbstractRegistrationsMustProvideFactory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not contact the Sitecore layout service.. + /// + internal static string Exception_CouldNotContactService { + get { + return ResourceManager.GetString("Exception_CouldNotContactService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not convert field of type {0} into type {1}. + /// + internal static string Exception_CouldNotConvertFieldToType { + get { + return ResourceManager.GetString("Exception_CouldNotConvertFieldToType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find converter for {0}.. + /// + internal static string Exception_CouldNotFindConverter { + get { + return ResourceManager.GetString("Exception_CouldNotFindConverter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected an array or object when deserializing a {0}. Found {1}.. + /// + internal static string Exception_DeserializationOfIncorrectToken { + get { + return ResourceManager.GetString("Exception_DeserializationOfIncorrectToken", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred with the Sitecore layout service.. + /// + internal static string Exception_GeneralServiceError { + get { + return ResourceManager.GetString("Exception_GeneralServiceError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Handler name cannot be null.. + /// + internal static string Exception_HandlerNameIsNull { + get { + return ResourceManager.GetString("Exception_HandlerNameIsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} key cannot be found in the handler registry.. + /// + internal static string Exception_HandlerRegistryKeyNotFound { + get { + return ResourceManager.GetString("Exception_HandlerRegistryKeyNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An invalid request was sent to the Sitecore layout service.. + /// + internal static string Exception_InvalidRequestError { + get { + return ResourceManager.GetString("Exception_InvalidRequestError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sitecore layout service returned a response in an invalid format.. + /// + internal static string Exception_InvalidResponseFormat { + get { + return ResourceManager.GetString("Exception_InvalidResponseFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sitecore layout service returned an item not found response.. + /// + internal static string Exception_ItemNotFoundError { + get { + return ResourceManager.GetString("Exception_ItemNotFoundError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sitecore layout service returned a server error.. + /// + internal static string Exception_LayoutServiceServerError { + get { + return ResourceManager.GetString("Exception_LayoutServiceServerError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while configuring the HTTP message.. + /// + internal static string Exception_MessageConfigurationError { + get { + return ResourceManager.GetString("Exception_MessageConfigurationError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Field could not be read as the type {0}. + /// + internal static string Exception_ReadingField { + get { + return ResourceManager.GetString("Exception_ReadingField", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can only register implementations of {0} as layout services.. + /// + internal static string Exception_RegisterTypesOfService { + get { + return ResourceManager.GetString("Exception_RegisterTypesOfService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HTTP Status Code. + /// + internal static string HttpStatusCode_KeyName { + get { + return ResourceManager.GetString("HttpStatusCode_KeyName", resourceCulture); + } + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.resx b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.resx new file mode 100644 index 0000000..c61eca9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Properties/Resources.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Abstract registrations must provide a factory to resolve a layout service. + + + Could not contact the Sitecore layout service. + + + Could not convert field of type {0} into type {1} + fromType,toType + + + Could not find converter for {0}. + objectType + + + Expected an array or object when deserializing a {0}. Found {1}. + expectedType, givenType + + + An error occurred with the Sitecore layout service. + + + Handler name cannot be null. + + + The {0} key cannot be found in the handler registry. + handlerName + + + An invalid request was sent to the Sitecore layout service. + + + The Sitecore layout service returned a response in an invalid format. + + + The Sitecore layout service returned an item not found response. + + + The Sitecore layout service returned a server error. + + + An error occurred while configuring the HTTP message. + + + The Field could not be read as the type {0} + type + + + Can only register implementations of {0} as layout services. + serviceType + + + HTTP Status Code + + \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs new file mode 100644 index 0000000..a3368c8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// +/// Initializes a new instance of the class. +/// +/// The to use for logging. +/// The graphQl client to handle response data. +/// The serializer to handle response data. +public class GraphQlLayoutServiceHandler( + IGraphQLClient client, + ISitecoreLayoutSerializer serializer, + ILogger logger) + : ILayoutRequestHandler +{ + private readonly ISitecoreLayoutSerializer _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IGraphQLClient _client = client ?? throw new ArgumentNullException(nameof(client)); + + /// + public async Task Request(SitecoreLayoutRequest request, string handlerName) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentException.ThrowIfNullOrWhiteSpace(handlerName); + + List errors = []; + SitecoreLayoutResponseContent? content = null; + + string? requestLanguage = request.Language(); + + if (string.IsNullOrWhiteSpace(requestLanguage)) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + GraphQLRequest layoutRequest = new() + { + Query = @" + query LayoutQuery($path: String!, $language: String!, $site: String!) { + layout(routePath: $path, language: $language, site: $site) { + item { + rendered + } + } + }", + OperationName = "LayoutQuery", + Variables = new + { + path = request.Path(), + language = requestLanguage, + site = request.SiteName() + } + }; + + GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Layout); + } + + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library + string? json = response.Data?.Layout?.Item?.Rendered.ToString(); + if (json == null) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = _serializer.Deserialize(json); + if (_logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + } + } + + if (response.Errors != null) + { + errors.AddRange( + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + } + } + + return new SitecoreLayoutResponse(request, errors) + { + Content = content, + Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) + }; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/ItemModel.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/ItemModel.cs new file mode 100644 index 0000000..59bcc40 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/ItemModel.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// Layout Service GraphQL Response item data. +/// +public class ItemModel +{ + /// + /// Gets or sets Rendered data. + /// + public JsonElement? Rendered { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutModel.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutModel.cs new file mode 100644 index 0000000..f5e508f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutModel.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// Layout Service GraphQL Response layout data. +/// +public class LayoutModel +{ + /// + /// Gets or sets Item Data. + /// + public ItemModel? Item { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutQueryResponse.cs new file mode 100644 index 0000000..c475d03 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutQueryResponse.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// Layout Service GraphQL Response. +/// +public class LayoutQueryResponse +{ + /// + /// Gets or sets Layout Service GraphQL Response. + /// + public LayoutModel? Layout { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs new file mode 100644 index 0000000..0f0a272 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs @@ -0,0 +1,18 @@ +using GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// +/// Initializes a new instance of the class. +/// +/// GraphQL Error of a GraphQL Query. +public class LayoutServiceGraphQlException(GraphQLError error) + : SitecoreLayoutServiceClientException(error.Message) +{ + /// + /// Gets GraphQL Error of a GraphQL Query. + /// + public GraphQLError GraphQlError { get; } = error ?? throw new ArgumentNullException(nameof(error)); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/HttpLayoutRequestHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/HttpLayoutRequestHandler.cs new file mode 100644 index 0000000..ea2efd6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/HttpLayoutRequestHandler.cs @@ -0,0 +1,180 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; + +/// +public class HttpLayoutRequestHandler : ILayoutRequestHandler +{ + private readonly ISitecoreLayoutSerializer _serializer; + private readonly HttpClient _client; + private readonly IOptionsSnapshot _options; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The to handle requests. + /// The serializer to handle response data. + /// An to access specific options for this instance. + /// The to use for logging. + public HttpLayoutRequestHandler( + HttpClient client, + ISitecoreLayoutSerializer serializer, + IOptionsSnapshot options, + ILogger logger) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + ArgumentNullException.ThrowIfNull(client.BaseAddress); + } + + /// + public async Task Request(SitecoreLayoutRequest request, string handlerName) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(handlerName); + + SitecoreLayoutResponseContent? content = null; + ILookup? metadata = null; + List errors = []; + + try + { + HttpLayoutRequestHandlerOptions options = _options.Get(handlerName); + HttpRequestMessage httpMessage; + try + { + httpMessage = BuildMessage(request, options); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Layout Service Http Request Message : {httpMessage}", httpMessage); + } + } + catch (Exception ex) + { + // an exception is recorded if there is an error configuring the HTTP message + errors = AddError(errors, new SitecoreLayoutServiceMessageConfigurationException(ex)); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("An error configuring the HTTP message : {ex}", ex); + } + + return new SitecoreLayoutResponse(request, errors); + } + + HttpResponseMessage httpResponse = await GetResponseAsync(httpMessage).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Layout Service Http Response : {httpResponse}", httpResponse); + } + + int responseStatusCode = (int)httpResponse.StatusCode; + if (!httpResponse.IsSuccessStatusCode) + { + errors = responseStatusCode switch + { + 404 => AddError(errors, new ItemNotFoundSitecoreLayoutServiceClientException(), responseStatusCode), + >= 400 and < 500 => AddError(errors, new InvalidRequestSitecoreLayoutServiceClientException(), responseStatusCode), + >= 500 => AddError(errors, new InvalidResponseSitecoreLayoutServiceClientException(new SitecoreLayoutServiceServerException()), responseStatusCode), + _ => AddError(errors, new SitecoreLayoutServiceClientException(), responseStatusCode), + }; + } + + if (httpResponse.IsSuccessStatusCode || httpResponse.StatusCode == HttpStatusCode.NotFound) + { + try + { + // content is only processed if a success or 404 status is returned + string json = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); + content = _serializer.Deserialize(json); + if (_logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + } + } + catch (Exception ex) + { + // an exception is recorded if there is a deserialization error + errors = AddError(errors, new InvalidResponseSitecoreLayoutServiceClientException(ex), responseStatusCode); + } + } + + try + { + metadata = httpResponse.Headers + .SelectMany(x => x.Value.Select(y => new { x.Key, Value = y })) + .ToLookup(k => k.Key, v => v.Value); + } + catch (Exception ex) + { + // an exception is recorded if there is an error reading the response headers + errors = AddError(errors, new InvalidResponseSitecoreLayoutServiceClientException(ex), responseStatusCode); + } + } + catch (Exception ex) + { + // an exception is recorded if there is a transport error + errors.Add(new CouldNotContactSitecoreLayoutServiceClientException(ex)); + } + + return new SitecoreLayoutResponse(request, errors) + { + Content = content, + Metadata = metadata + }; + } + + /// + /// Build a new using the layout request and handler options provided. + /// + /// The . + /// The . + /// A configured . + protected virtual HttpRequestMessage BuildMessage(SitecoreLayoutRequest request, HttpLayoutRequestHandlerOptions? options) + { + HttpRequestMessage message = new(HttpMethod.Get, _client.BaseAddress); + + if (options != null) + { + foreach (Action map in options.RequestMap) + { + map(request, message); + } + } + + return message; + } + + /// + /// Get the returned by the provided URI. + /// + /// The to be sent to the provided URI. + /// A . + protected virtual async Task GetResponseAsync(HttpRequestMessage message) + { + return await _client.SendAsync(message).ConfigureAwait(false); + } + + private static List AddError(List errors, SitecoreLayoutServiceClientException error, int statusCode = 0) + { + if (statusCode > 0) + { + error.Data.Add(Resources.HttpStatusCode_KeyName, statusCode); + } + + errors.Add(error); + return errors; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/RequestKeys.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/RequestKeys.cs new file mode 100644 index 0000000..21920e2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/RequestKeys.cs @@ -0,0 +1,52 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +/// +/// Defines the keys that may be included in the Sitecore layout service request. +/// +public static class RequestKeys +{ + /// + /// The key name for request site name. + /// + public const string SiteName = "sc_site"; + + /// + /// The key name for request path. + /// + public const string Path = "item"; + + /// + /// The key name for request language. + /// + public const string Language = "sc_lang"; + + /// + /// The key name for request API key. + /// + public const string ApiKey = "sc_apikey"; + + /// + /// The key name for request mode. + /// + public const string Mode = "sc_mode"; + + /// + /// The key name for device ID. + /// + public const string Device = "sc_device"; + + /// + /// The key name for request item ID. + /// + public const string ItemId = "sc_itemid"; + + /// + /// The key name for request authentication header. + /// + public const string AuthHeaderKey = "sc_auth_header_key"; + + /// + /// The key name for request preview date. + /// + public const string PreviewDate = "sc_date"; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequest.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequest.cs new file mode 100644 index 0000000..14102c5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequest.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +/// +/// Represents Sitecore layout service request data. +/// +public class SitecoreLayoutRequest : Dictionary +{ + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutRequest() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// + /// Safely gets a typed value from the underlying dictionary. + /// + /// The type to be resolved. + /// The key to be located. + /// The discovered value. + /// True if successful, otherwise false. + public virtual bool TryReadValue(string key, out T? value) + { + ArgumentNullException.ThrowIfNull(key); + + value = default; + if (TryGetValue(key, out object? result) && result != null) + { + if (result is T typed) + { + value = typed; + } + else + { + try + { + value = ConvertValue(result); + } + catch + { + return false; + } + } + + return true; + } + + return false; + } + + /// + /// Converts the given value to type . + /// + /// The destination type. + /// The input value. + /// An instance of if successful. + protected virtual T? ConvertValue(object value) + { + TypeConverter? converter = TypeDescriptor.GetConverter(typeof(T)); + return converter == null + ? throw new InvalidOperationException(string.Format(Resources.Exception_CouldNotFindConverter, typeof(T))) + : (T?)converter.ConvertFrom(value); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequestExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequestExtensions.cs new file mode 100644 index 0000000..1973a64 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/SitecoreLayoutRequestExtensions.cs @@ -0,0 +1,244 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +/// +/// Extension methods for . +/// +public static class SitecoreLayoutRequestExtensions +{ + /// + /// The key name for request headers data. + /// + private const string HeadersKey = "sc_request_headers_key"; + + /// + /// Gets the API key of the request. + /// + /// The . + /// The API key string value, otherwise null. + public static string? ApiKey(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.ApiKey); + + /// + /// Sets the API key of the request. + /// If a null value is provided, the API key is removed from the request. + /// + /// The . + /// The value to set. + /// The . + public static SitecoreLayoutRequest ApiKey(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.ApiKey, value); + + /// + /// Gets the site name of the request. + /// + /// The . + /// The site name string value, otherwise null. + public static string? SiteName(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.SiteName); + + /// + /// Sets the site name of the request. + /// If a null value is provided, the site name is removed from the request. + /// + /// The . + /// The value to set. + /// The . + public static SitecoreLayoutRequest SiteName(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.SiteName, value); + + /// + /// Gets the language of the request. + /// + /// The . + /// The language string value, otherwise null. + public static string? Language(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.Language); + + /// + /// Sets the language of the request. + /// If a null value is provided, the language is removed from the request. + /// + /// The . + /// The value to set. + /// The . + public static SitecoreLayoutRequest Language(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.Language, value); + + /// + /// Gets the path of the request. + /// + /// The . + /// The path string value, otherwise null. + public static string? Path(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.Path); + + /// + /// Sets the path of the request. + /// If a null value is provided, the path is removed from the request. + /// + /// The . + /// The value to set. + /// The . + public static SitecoreLayoutRequest Path(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.Path, value); + + /// + /// Gets the mode of the request. + /// + /// The . + /// The mode string value, otherwise null. + public static string? Mode(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.Mode); + + /// + /// Sets the mode of the request. + /// If a null value is provided, the mode is removed from the request. + /// + /// The . + /// The value to set. + /// The . + public static SitecoreLayoutRequest Mode(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.Mode, value); + + /// + /// Sets the preview date of the request. + /// If a null value is provided, the preview date is removed from the request. + /// + /// The . + /// The . + public static string? PreviewDate(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.PreviewDate); + + /// + /// Gets the preview date of the request. + /// + /// The . + /// The value to set. + /// The preview date string value, otherwise null. + public static SitecoreLayoutRequest PreviewDate(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.PreviewDate, value); + + /// + /// Sets the authentication header of the request. + /// If a null value is provided, the authentication header is removed from the request. + /// + /// The . + /// The . + public static string? AuthenticationHeader(this SitecoreLayoutRequest request) + => ReadValue(request, RequestKeys.AuthHeaderKey); + + /// + /// Gets the authentication header of the request. + /// + /// The . + /// The value to set. + /// The authentication header string value, otherwise null. + public static SitecoreLayoutRequest AuthenticationHeader(this SitecoreLayoutRequest request, string? value) + => WriteValue(request, RequestKeys.AuthHeaderKey, value); + + /// + /// Update missing values in the original request with values in the default request. + /// + /// The original request object. + /// The default request object. + /// The updated request object. + public static SitecoreLayoutRequest UpdateRequest(this SitecoreLayoutRequest request, Dictionary? requestDefaults) + { + ArgumentNullException.ThrowIfNull(request); + + if (requestDefaults == null || requestDefaults.Count == 0) + { + return request; + } + + // Create a base SitecoreLayoutRequest from the fallback parameters provided. + SitecoreLayoutRequest baseRequest = []; + + foreach (KeyValuePair entry in requestDefaults) + { + baseRequest[entry.Key] = entry.Value; + } + + // Create the final request to be returned using the base request as the starting point + SitecoreLayoutRequest mergedRequest = baseRequest; + + foreach (KeyValuePair entry in request) + { + if (entry.Value == null) + { + mergedRequest.Remove(entry.Key); + } + else + { + mergedRequest[entry.Key] = entry.Value; + } + } + + return mergedRequest; + } + + /// + /// Adds the header with and to the headers collection stored in layout request. + /// + /// Layout request instance. + /// Header key. + /// header value. + public static void AddHeader(this SitecoreLayoutRequest request, string key, string[] value) + { + ArgumentNullException.ThrowIfNull(request); + + if (!TryGetHeadersCollection(request, out Dictionary? headers)) + { + headers = []; + request.Add(HeadersKey, headers); + } + + if (headers != null && !headers.TryAdd(key, value)) + { + string[] prev = headers[key]; + headers[key] = [.. prev, .. value]; + } + } + + /// + /// Adds headers collection to the headers collection stored in layout request. + /// + /// Layout request instance. + /// Headers collection. + public static void AddHeaders(this SitecoreLayoutRequest request, IDictionary headers) + { + ArgumentNullException.ThrowIfNull(request); + + foreach (KeyValuePair h in headers) + { + request.AddHeader(h.Key, h.Value); + } + } + + /// + /// Tries to get headers collection from layout request. + /// + /// Layout request instance. + /// Headers. + /// false if there is no headers collection otherwise true. + public static bool TryGetHeadersCollection(this SitecoreLayoutRequest request, out Dictionary? headers) + { + ArgumentNullException.ThrowIfNull(request); + return request.TryReadValue(HeadersKey, out headers); + } + + private static T? ReadValue(SitecoreLayoutRequest request, string key) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentException.ThrowIfNullOrWhiteSpace(key); + return request.TryReadValue(key, out T? result) ? result : default; + } + + private static SitecoreLayoutRequest WriteValue(SitecoreLayoutRequest request, string key, T? value) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentException.ThrowIfNullOrWhiteSpace(key); + request[key] = value; + return request; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Component.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Component.cs new file mode 100644 index 0000000..27e5afa --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Component.cs @@ -0,0 +1,41 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents component information for a Sitecore layout service response. +/// +public class Component : FieldsReader, IPlaceholderFeature +{ + /// + /// Gets or sets the ID of the component. + /// + [DataMember(Name = "uid")] + [JsonPropertyName("uid")] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the name of the component. + /// + [DataMember(Name = "componentName")] + [JsonPropertyName("componentName")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the datasource of the component. + /// + public string DataSource { get; set; } = "available-in-connected-mode"; + + /// + /// Gets the parameters for the component. + /// + [DataMember(Name = "params")] + [JsonPropertyName("params")] + public Dictionary Parameters { get; init; } = []; + + /// + /// Gets the placeholders for the component. + /// + public Dictionary Placeholders { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Context.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Context.cs new file mode 100644 index 0000000..35a9575 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Context.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents the context of a Sitecore layout response. +/// +public class Context +{ + /// + /// Gets or sets a value indicating whether the page is in editing mode. + /// + [DataMember(Name = "pageEditing")] + [JsonPropertyName("pageEditing")] + public bool IsEditing { get; set; } + + /// + /// Gets or sets the of the response. + /// + public Site? Site { get; set; } + + /// + /// Gets or sets the of the response. + /// + public PageState? PageState { get; set; } + + /// + /// Gets or sets the language of the response. + /// + public string Language { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableChrome.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableChrome.cs new file mode 100644 index 0000000..a5b2ebe --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableChrome.cs @@ -0,0 +1,33 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents chrome information for a Sitecore layout service response. +/// +public class EditableChrome : IPlaceholderFeature +{ + /// + /// Gets or sets the name of the chrome. + /// + [DataMember] + public string Name { get; set; } = LayoutServiceClientConstants.SitecoreChromes.ChromeTag; + + /// + /// Gets or sets the type of the chrome. + /// + public string Type { get; set; } = LayoutServiceClientConstants.SitecoreChromes.ChromeTypeValue; + + /// + /// Gets or sets the content of the chrome. + /// + [DataMember(Name = "contents")] + [JsonPropertyName("contents")] + public string Content { get; set; } = string.Empty; + + /// + /// Gets the attributes for the chrome. + /// + public Dictionary Attributes { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs new file mode 100644 index 0000000..62fa1d3 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents an arbitrary field in a Sitecore layout service response that contains a value that can be edited. +/// +/// The value type. +public class EditableField + : Field, IEditableField +{ + /// + [DataMember(Name = "editable")] + [JsonPropertyName("editable")] + public string EditableMarkup { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Field.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Field.cs new file mode 100644 index 0000000..b3c6528 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Field.cs @@ -0,0 +1,20 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Base implementation of an . +/// +public abstract class Field : FieldReader, IField +{ + /// + protected override object HandleRead(Type type) + { + if (type.IsInstanceOfType(this)) + { + return this; + } + + throw new InvalidCastException(string.Format(Resources.Exception_CouldNotConvertFieldToType, GetType(), type)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldOfTValue.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldOfTValue.cs new file mode 100644 index 0000000..f0d64a5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldOfTValue.cs @@ -0,0 +1,13 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents an arbitrary field in a Sitecore layout service response that contains a value. +/// +/// The value type. +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Generic Type.")] +public class Field + : Field, IValueField +{ + /// + public required TValue Value { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldReader.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldReader.cs new file mode 100644 index 0000000..e21760b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldReader.cs @@ -0,0 +1,76 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Implements reading of a Field as a specified type. +/// +public abstract class FieldReader : IFieldReader +{ + /// + public virtual TField? Read() + where TField : IField + { + return (TField?)Read(typeof(TField)); + } + + /// + public virtual bool TryRead(out TField? field) + where TField : IField + { + if (TryRead(typeof(TField), out IField? resultField)) + { + field = (TField?)resultField; + return true; + } + + field = default; + return false; + } + + /// + public virtual bool TryRead(Type type, out IField? field) + { + ArgumentNullException.ThrowIfNull(type); + + bool result = false; + field = default; + + if (typeof(IField).IsAssignableFrom(type)) + { + try + { + field = HandleRead(type) as IField; + result = true; + } + catch + { + result = false; + } + } + + return result; + } + + /// + public virtual object? Read(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + try + { + return HandleRead(type); + } + catch (Exception ex) + { + throw new FieldReaderException(type, ex); + } + } + + /// + /// Returns an instance of the Field data as a specified type. + /// + /// The type to read the field as. + /// A new instance of the specified type. + protected abstract object? HandleRead(Type type); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/CheckboxField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/CheckboxField.cs new file mode 100644 index 0000000..fee0220 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/CheckboxField.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a checkbox field. +/// +public class CheckboxField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + [SetsRequiredMembers] + public CheckboxField() + { + Value = default; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public CheckboxField(bool value) + { + Value = value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListField.cs new file mode 100644 index 0000000..eabc8c0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListField.cs @@ -0,0 +1,92 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a content list field. +/// +public class ContentListField : List, IField, IFieldReader +{ + /// + /// Initializes a new instance of the class. + /// + public ContentListField() + { + } + + /// + public virtual TField Read() + where TField : IField + { + return (TField)Read(typeof(TField)); + } + + /// + public virtual bool TryRead(out TField? field) + where TField : IField + { + if (TryRead(typeof(TField), out IField? resultField)) + { + field = (TField?)resultField; + return true; + } + + field = default; + return false; + } + + /// + public virtual bool TryRead(Type type, out IField? field) + { + ArgumentNullException.ThrowIfNull(type); + + bool result = false; + field = default; + + if (typeof(IField).IsAssignableFrom(type)) + { + try + { + field = HandleRead(type) as IField; + result = true; + } + catch + { + result = false; + } + } + + return result; + } + + /// + public virtual object Read(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + try + { + return HandleRead(type); + } + catch (Exception ex) + { + throw new FieldReaderException(type, ex); + } + } + + /// + /// Returns an instance of the Field data as a specified type. + /// + /// The type to read the field as. + /// A new instance of the specified type. + protected virtual object HandleRead(Type type) + { + if (type.IsAssignableFrom(GetType())) + { + return this; + } + + throw new InvalidCastException(string.Format(Resources.Exception_CouldNotConvertFieldToType, GetType(), type)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListFieldOfTTargetModel.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListFieldOfTTargetModel.cs new file mode 100644 index 0000000..5a6ce98 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ContentListFieldOfTTargetModel.cs @@ -0,0 +1,87 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Allows list field types to be mapped to a specific model. +/// +/// Strongly typed model to map the target of each list item to. +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Generic Type.")] +public class ContentListField : List>, IField, IFieldReader + where TTargetModel : class +{ + /// + public virtual TField Read() + where TField : IField + { + return (TField)Read(typeof(TField)); + } + + /// + public virtual bool TryRead(out TField? field) + where TField : IField + { + if (TryRead(typeof(TField), out IField? resultField)) + { + field = (TField?)resultField; + return true; + } + + field = default; + return false; + } + + /// + public virtual bool TryRead(Type type, out IField? field) + { + ArgumentNullException.ThrowIfNull(type); + bool result = false; + field = default; + + if (typeof(IField).IsAssignableFrom(type)) + { + try + { + field = HandleRead(type) as IField; + result = true; + } + catch + { + result = false; + } + } + + return result; + } + + /// + public virtual object Read(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + try + { + return HandleRead(type); + } + catch (Exception ex) + { + throw new FieldReaderException(type, ex); + } + } + + /// + /// Returns an instance of the Field data as a specified type. + /// + /// The type to read the field as. + /// A new instance of the specified type. + protected virtual object HandleRead(Type type) + { + if (type.IsAssignableFrom(GetType())) + { + return this; + } + + throw new InvalidCastException(string.Format(Resources.Exception_CouldNotConvertFieldToType, GetType(), type)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/DateField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/DateField.cs new file mode 100644 index 0000000..735edba --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/DateField.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a date field. +/// +public class DateField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + [SetsRequiredMembers] + public DateField() + { + Value = DateTime.MinValue; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public DateField(DateTime value) + { + ArgumentNullException.ThrowIfNull(value); + Value = value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/FileField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/FileField.cs new file mode 100644 index 0000000..76ba87a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/FileField.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using File = Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties.File; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a file field. +/// +public class FileField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + public FileField() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public FileField(File value) + { + ArgumentNullException.ThrowIfNull(value); + Value = value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/HyperLinkField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/HyperLinkField.cs new file mode 100644 index 0000000..87af503 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/HyperLinkField.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a hyperlink field. +/// +public class HyperLinkField : WrappedEditableField +{ + /// + /// Initializes a new instance of the class. + /// + public HyperLinkField() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public HyperLinkField(HyperLink value) + { + ArgumentNullException.ThrowIfNull(value); + Value = value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ImageField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ImageField.cs new file mode 100644 index 0000000..1393135 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ImageField.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents an image field. +/// +public class ImageField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + public ImageField() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public ImageField(Image value) + { + ArgumentNullException.ThrowIfNull(value); + Value = value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkField.cs new file mode 100644 index 0000000..1ae78f0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkField.cs @@ -0,0 +1,95 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents an item link field. +/// +public class ItemLinkField : FieldsReader, IField, IFieldReader +{ + /// + /// Gets or sets the ID for this . + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the URL for this . + /// + public string? Url { get; set; } + + /// + public virtual TField Read() + where TField : IField + { + return (TField)Read(typeof(TField)); + } + + /// + public virtual bool TryRead(out TField? field) + where TField : IField + { + if (TryRead(typeof(TField), out IField? resultField)) + { + field = (TField?)resultField; + return true; + } + + field = default; + return false; + } + + /// + public virtual bool TryRead(Type type, out IField? field) + { + ArgumentNullException.ThrowIfNull(type); + + bool result = false; + field = default; + + if (typeof(IField).IsAssignableFrom(type)) + { + try + { + field = HandleRead(type) as IField; + result = true; + } + catch + { + result = false; + } + } + + return result; + } + + /// + public virtual object Read(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + try + { + return HandleRead(type); + } + catch (Exception ex) + { + throw new FieldReaderException(type, ex); + } + } + + /// + /// Returns an instance of the Field data as a specified type. + /// + /// The type to read the field as. + /// A new instance of the specified type. + protected virtual object HandleRead(Type type) + { + if (type.IsAssignableFrom(GetType())) + { + return this; + } + + throw new InvalidCastException(string.Format(Resources.Exception_CouldNotConvertFieldToType, GetType(), type)); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkFieldOfTTargetModel.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkFieldOfTTargetModel.cs new file mode 100644 index 0000000..cc1643f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/ItemLinkFieldOfTTargetModel.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Facilitates mapping a link field to a specific target model type. +/// +/// Target model type. +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Generic Type.")] +public class ItemLinkField : ItemLinkField + where TTargetModel : class +{ + /// + /// Gets or sets the strongly types target if the item link. + /// + public TTargetModel? Target { get; protected set; } + + /// + /// Gets or sets the strongly typed target. + /// + // This property overrides the fields property in the base class and allows the JSON serializer to convert it to a strongly typed + // TTargetModel. The deserialized class is stored in the Target property, so it makes the developer code make more sense. + public new TTargetModel? Fields { get => Target; set => Target = value; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/NumberField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/NumberField.cs new file mode 100644 index 0000000..a6c10fd --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/NumberField.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a number field. +/// +public class NumberField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + public NumberField() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public NumberField(double value) + { + ArgumentNullException.ThrowIfNull(value); + Value = Convert.ToDecimal(value); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/RichTextField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/RichTextField.cs new file mode 100644 index 0000000..b76f209 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/RichTextField.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a textfield that supports HTML. +/// +public class RichTextField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + [SetsRequiredMembers] + public RichTextField() + { + Value = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + /// True if the value is encoded, otherwise false. Defaults to true. + [SetsRequiredMembers] + public RichTextField(string value, bool encoded = true) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - Guard to ensure the contract + value ??= string.Empty; + Value = Build(value, encoded); + } + + private static string Build(string value, bool encoded) + { + return encoded ? System.Web.HttpUtility.UrlDecode(value) : value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/TextField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/TextField.cs new file mode 100644 index 0000000..3832ef6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Fields/TextField.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +/// +/// Represents a text field. +/// +public class TextField : EditableField +{ + /// + /// Initializes a new instance of the class. + /// + [SetsRequiredMembers] + public TextField() + { + Value = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + [SetsRequiredMembers] + public TextField(string value) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - Guard to ensure the contract + Value = value ?? string.Empty; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldsReader.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldsReader.cs new file mode 100644 index 0000000..76984e5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/FieldsReader.cs @@ -0,0 +1,167 @@ +using System.Reflection; +using System.Xml.Linq; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Properties; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Implements reading of a collection of fields as a specified type. +/// +public abstract class FieldsReader : IFieldsReader +{ + /// + public Dictionary Fields { get; set; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Reads a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// A new instance of . + public virtual TField? ReadField(string name) + where TField : IField + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + return (TField?)ReadField(typeof(TField), name); + } + + /// + /// Attempts to read a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// The resulting instance if successful. + /// True if successful, otherwise false. + public virtual bool TryReadField(string name, out TField? instance) + where TField : IField + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + if (TryReadField(typeof(TField), name, out object? resultInstance)) + { + instance = (TField?)resultInstance; + return true; + } + + instance = default; + return false; + } + + /// + /// Reads the collection of fields as the specified type. + /// + /// The type of object to return. + /// A new instance of . + public virtual T? ReadFields() + where T : new() + { + return (T?)ReadFields(typeof(T)); + } + + /// + /// Attempts to read the collection of fields as the specified type. + /// + /// The type of object to return. + /// The resulting instance if successful. + /// True if successful, otherwise false. + public virtual bool TryReadFields(out T? instance) + where T : new() + { + if (TryReadFields(typeof(T), out object? resultInstance)) + { + instance = (T?)resultInstance; + return true; + } + + instance = default; + return false; + } + + /// + public virtual object? ReadField(Type type, string name) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + IFieldReader? field = Fields + .FirstOrDefault(x => x.Key.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Value; + + return field?.Read(type); + } + + /// + public virtual bool TryReadField(Type type, string name, out object? instance) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + instance = default; + bool result; + + try + { + instance = ReadField(type, name); + result = instance != null; + } + catch + { + result = false; + } + + return result; + } + + /// + public bool TryReadFields(Type type, out object? instance) + { + ArgumentNullException.ThrowIfNull(type); + + instance = default; + bool result; + + try + { + instance = HandleReadFields(type); + result = true; + } + catch + { + result = false; + } + + return result; + } + + /// + public object? ReadFields(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + try + { + return HandleReadFields(type); + } + catch (Exception ex) + { + throw new InvalidCastException(string.Format(Resources.Exception_CouldNotConvertFieldToType, GetType(), type), ex); + } + } + + /// + /// Handles reading the field collection, binding to the specified type. + /// + /// The type to be bound. + /// A new instance of the specified type, if successful. + protected virtual object? HandleReadFields(Type type) + { + object? instance = Activator.CreateInstance(type); + + foreach (PropertyInfo prop in type.GetProperties().Where(x => x.CanWrite)) + { + prop.SetValue(instance, ReadField(prop.PropertyType, prop.Name)); + } + + return instance; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs new file mode 100644 index 0000000..fcdda5c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Exposes HTML markup values for an editable . +/// +public interface IEditableField : IField +{ + /// + /// Gets or sets the HTML markup for this when editing. + /// + public string EditableMarkup { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IField.cs new file mode 100644 index 0000000..fa1011b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IField.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Marker interface to identify Field types. +/// +public interface IField; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldReader.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldReader.cs new file mode 100644 index 0000000..59e8360 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldReader.cs @@ -0,0 +1,41 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Supports delayed reading of an . +/// +public interface IFieldReader +{ + /// + /// Reads the current Field as the specified type. + /// + /// The type of Field to be read. + /// A new instance of . + TField? Read() + where TField : IField; + + /// + /// Attempts to read the current Field as the specified type. + /// + /// The type of Field to be read. + /// The resulting instance if successful. + /// True if the field could be read as the specified type, otherwise false. + bool TryRead(out TField? field) + where TField : IField; + + /// + /// Reads the current Field as the specified type. + /// The type must implement . + /// + /// The type of field to be read. + /// A new instance if successful. + object? Read(Type type); + + /// + /// Attempts to read the current Field as the specified type. + /// The type must implement . + /// + /// The type of field to be read. + /// The resulting field. + /// True if the field could be read as the specified type, otherwise false. + bool TryRead(Type type, out IField? field); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldsReader.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldsReader.cs new file mode 100644 index 0000000..fac2aa9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IFieldsReader.cs @@ -0,0 +1,80 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Exposes a collection of . +/// +public interface IFieldsReader +{ + /// + /// Gets or sets the Fields associated with this instance. + /// + Dictionary Fields { get; set; } + + /// + /// Reads a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// A new instance of . + TField? ReadField(string name) + where TField : IField; + + /// + /// Attempts to read a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// The resulting instance if successful. + /// True if successful, otherwise false. + bool TryReadField(string name, out TField? instance) + where TField : IField; + + /// + /// Reads the collection of fields as the specified type. + /// + /// The type of object to return. + /// A new instance of . + T? ReadFields() + where T : new(); + + /// + /// Attempts to read the collection of fields as the specified type. + /// + /// The type of object to return. + /// The resulting instance if successful. + /// True if successful, otherwise false. + bool TryReadFields(out T? instance) + where T : new(); + + /// + /// Reads a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// A new instance of the specified type. + object? ReadField(Type type, string name); + + /// + /// Attempts to read a field from the collection as the specified type. + /// + /// The type of object to return. + /// The name of the field to be read. + /// A new instance of if successful. + /// True if successful, otherwise false. + bool TryReadField(Type type, string name, out object? instance); + + /// + /// Reads the collection of fields as the specified type. + /// + /// The type of object to return. + /// A new instance of the specified type. + object? ReadFields(Type type); + + /// + /// Attempts to read the collection of fields as the specified type. + /// + /// The type of object to return. + /// A new instance of if successful. + /// True if successful, otherwise false. + bool TryReadFields(Type type, out object? instance); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IPlaceholderFeature.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IPlaceholderFeature.cs new file mode 100644 index 0000000..6316029 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IPlaceholderFeature.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Marker interface for Sitecore layout components and chromes. +/// +public interface IPlaceholderFeature; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IValueField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IValueField.cs new file mode 100644 index 0000000..3e00cfc --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IValueField.cs @@ -0,0 +1,13 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Exposes a Value property on an . +/// +/// The value type. +public interface IValueField : IField +{ + /// + /// Gets or sets the value of the . + /// + TValue Value { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IWrappedEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IWrappedEditableField.cs new file mode 100644 index 0000000..853af1d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IWrappedEditableField.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Exposes HTML markup values for an editable that has wrapping editable markup. +/// +public interface IWrappedEditableField : IField +{ + /// + /// Gets or sets the HTML markup to render before this when editing. + /// + string EditableMarkupFirst { get; set; } + + /// + /// Gets or sets the HTML markup to render after this when editing. + /// + string EditableMarkupLast { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Placeholder.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Placeholder.cs new file mode 100644 index 0000000..137c286 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Placeholder.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents the placeholder feature collection for a Sitecore layout service response. +/// +public class Placeholder : List; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/PlaceholderExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/PlaceholderExtensions.cs new file mode 100644 index 0000000..a1828d2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/PlaceholderExtensions.cs @@ -0,0 +1,52 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Extension methods for . +/// +public static class PlaceholderExtensions +{ + /// + /// Returns the from the specified index in the placeholder feature collection. + /// + /// The placeholder feature collection. + /// The index of the component to be returned. + /// A . + public static Component? ComponentAt(this Placeholder placeholder, int index) + { + ArgumentNullException.ThrowIfNull(placeholder); + return placeholder.FeatureAt(index); + } + + /// + /// Returns the from the specified index in the placeholder feature collection. + /// + /// The placeholder feature collection. + /// The index of the chrome to be returned. + /// An . + public static EditableChrome? ChromeAt(this Placeholder placeholder, int index) + { + ArgumentNullException.ThrowIfNull(placeholder); + return placeholder.FeatureAt(index); + } + + /// + /// Returns the from the specified index in the placeholder feature collection. + /// + /// The placeholder feature collection. + /// The index of the feature to be returned. + /// The type of the placeholder feature. + /// An . + private static T? FeatureAt(this Placeholder placeholder, int index) + where T : class, IPlaceholderFeature + { + ArgumentNullException.ThrowIfNull(placeholder); + + IPlaceholderFeature feature = placeholder.ElementAt(index); + if (feature is T placeholderFeature) + { + return placeholderFeature; + } + + return default; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/CachingData.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/CachingData.cs new file mode 100644 index 0000000..99c1993 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/CachingData.cs @@ -0,0 +1,66 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +/// +/// Represents the caching information for a device rendering returned in a Sitecore layout service response. +/// +public class CachingData +{ + /// + /// Gets or sets the 'cacheable' flag. + /// + [DataMember(Name = "cacheable")] + [JsonPropertyName("cacheable")] + public bool? Cacheable { get; set; } + + /// + /// Gets or sets the 'vary by data' flag. + /// + [DataMember(Name = "varyByData")] + [JsonPropertyName("varyByData")] + public bool? VaryByData { get; set; } + + /// + /// Gets or sets the 'vary by device' flag. + /// + [DataMember(Name = "varyByDevice")] + [JsonPropertyName("varyByDevice")] + public bool? VaryByDevice { get; set; } + + /// + /// Gets or sets the 'vary by login' flag. + /// + [DataMember(Name = "varyByLogin")] + [JsonPropertyName("varyByLogin")] + public bool? VaryByLogin { get; set; } + + /// + /// Gets or sets the 'vary by parameters' flag. + /// + [DataMember(Name = "varyByParameters")] + [JsonPropertyName("varyByParameters")] + public bool? VaryByParameters { get; set; } + + /// + /// Gets or sets the 'vary by query string' flag. + /// + [DataMember(Name = "varyByQueryString")] + [JsonPropertyName("varyByQueryString")] + public bool? VaryByQueryString { get; set; } + + /// + /// Gets or sets the 'vary by user' flag. + /// + [DataMember(Name = "varyByUser")] + [JsonPropertyName("varyByUser")] + public bool? VaryByUser { get; set; } + + /// + /// Gets or sets the 'clear on index update' flag. + /// + [DataMember(Name = "clearOnIndexUpdate")] + [JsonPropertyName("clearOnIndexUpdate")] + public bool? ClearOnIndexUpdate { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Device.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Device.cs new file mode 100644 index 0000000..fdc5ee1 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Device.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +/// +/// Represents a presentation device returned in a Sitecore layout service response. +/// +public class Device +{ + /// + /// Gets or sets the device ID. + /// + [DataMember(Name = "id")] + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// Gets or sets the device layout ID. + /// + [DataMember(Name = "layoutId")] + [JsonPropertyName("layoutId")] + public string? LayoutId { get; set; } + + /// + /// Gets or sets the list of placeholder details. + /// + [DataMember(Name = "placeholders")] + [JsonPropertyName("placeholders")] + public List Placeholders { get; set; } = []; + + /// + /// Gets or sets the list of renderings. + /// + [DataMember(Name = "renderings")] + [JsonPropertyName("renderings")] + public List Renderings { get; set; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Personalization.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Personalization.cs new file mode 100644 index 0000000..c64653e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Personalization.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +/// +/// Represents the personalization information for a device rendering returned in a Sitecore layout service response. +/// +public class Personalization +{ + /// + /// Gets or sets the rules. + /// + [DataMember(Name = "rules")] + [JsonPropertyName("rules")] + public string? Rules { get; set; } + + /// + /// Gets or sets the conditions. + /// + [DataMember(Name = "conditions")] + [JsonPropertyName("conditions")] + public string? Conditions { get; set; } + + /// + /// Gets or sets the multivariate test ID. + /// + [DataMember(Name = "multiVariateTestId")] + [JsonPropertyName("multiVariateTestId")] + public string? MultiVariateTestId { get; set; } + + /// + /// Gets or sets the personalization test. + /// + [DataMember(Name = "personalizationTest")] + [JsonPropertyName("personalizationTest")] + public string? PersonalizationTest { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/PlaceholderData.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/PlaceholderData.cs new file mode 100644 index 0000000..6c763e0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/PlaceholderData.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +/// +/// Represents the placeholder information for a presentation device returned in a Sitecore layout service response. +/// +public class PlaceholderData +{ + /// + /// Gets or sets the placeholder key. + /// + [DataMember(Name = "key")] + [JsonPropertyName("key")] + public string? Key { get; set; } + + /// + /// Gets or sets the instance ID. + /// + [DataMember(Name = "instanceId")] + [JsonPropertyName("instanceId")] + public string? InstanceId { get; set; } + + /// + /// Gets or sets the metadata ID. + /// + [DataMember(Name = "metadataId")] + [JsonPropertyName("metadataId")] + public string? MetadataId { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Rendering.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Rendering.cs new file mode 100644 index 0000000..bef66a0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Presentation/Rendering.cs @@ -0,0 +1,59 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +/// +/// Represents the rendering information for a presentation device returned in a Sitecore layout service response. +/// +public class Rendering +{ + /// + /// Gets or sets the rendering ID. + /// + [DataMember(Name = "id")] + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// + /// Gets or sets the instance ID. + /// + [DataMember(Name = "instanceId")] + [JsonPropertyName("instanceId")] + public string? InstanceId { get; set; } + + /// + /// Gets or sets the placeholder key. + /// + [DataMember(Name = "placeholderKey")] + [JsonPropertyName("placeholderKey")] + public string PlaceholderKey { get; set; } = string.Empty; + + /// + /// Gets or sets the data source. + /// + [DataMember(Name = "dataSource")] + [JsonPropertyName("dataSource")] + public string? DataSource { get; set; } + + /// + /// Gets or sets the parameters. + /// + [DataMember(Name = "parameters")] + [JsonPropertyName("parameters")] + public Dictionary Parameters { get; set; } = []; + + /// + /// Gets or sets the caching details. + /// + [DataMember(Name = "caching")] + [JsonPropertyName("caching")] + public CachingData? Caching { get; set; } + + /// + /// Gets or sets the personalization details. + /// + [DataMember(Name = "analytics")] + [JsonPropertyName("analytics")] + public Personalization? Personalization { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/File.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/File.cs new file mode 100644 index 0000000..17612a2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/File.cs @@ -0,0 +1,52 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +/// +/// Represents the properties of a file in a Sitecore layout service response. +/// +public class File +{ + /// + /// Gets or sets the source of the file. + /// + public string? Src { get; set; } + + /// + /// Gets or sets the title of the file. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the description of the file. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the name of the file. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the display name of the file. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets the keywords for the file. + /// + public string? Keywords { get; set; } + + /// + /// Gets or sets the extension of the file. + /// + public string? Extension { get; set; } + + /// + /// Gets or sets the mime type of the file. + /// + public string? MimeType { get; set; } + + /// + /// Gets or sets the size of the file. + /// + public long? Size { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/HyperLink.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/HyperLink.cs new file mode 100644 index 0000000..6669797 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/HyperLink.cs @@ -0,0 +1,50 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +/// +/// Represents the properties of a hyperlink in a Sitecore layout service response. +/// +public class HyperLink +{ + /// + /// Gets or sets the href of the hyperlink. + /// + public string? Href { get; set; } + + /// + /// Gets or sets the text of the hyperlink. + /// + [DataMember(EmitDefaultValue = false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Text { get; set; } + + /// + /// Gets or sets the target of the hyperlink. + /// + [DataMember(EmitDefaultValue = false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Target { get; set; } + + /// + /// Gets or sets the CSS class of the hyperlink. + /// + [DataMember(EmitDefaultValue = false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Class { get; set; } + + /// + /// Gets or sets the title of the hyperlink. + /// + [DataMember(EmitDefaultValue = false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Title { get; set; } + + /// + /// Gets or sets the anchor of the hyperlink. + /// + [DataMember(EmitDefaultValue = false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Anchor { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/Image.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/Image.cs new file mode 100644 index 0000000..3cf7672 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Properties/Image.cs @@ -0,0 +1,52 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +/// +/// Represents the properties of an image in a Sitecore layout service response. +/// +public class Image +{ + /// + /// Gets or sets the source of the image. + /// + public string? Src { get; set; } + + /// + /// Gets or sets the alternative text of the image. + /// + public string? Alt { get; set; } + + /// + /// Gets or sets the height of the image. + /// + public int? Height { get; set; } + + /// + /// Gets or sets the width of the image. + /// + public int? Width { get; set; } + + /// + /// Gets or sets the title of the image. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the number of whitespaces on the left and the right side of the image. + /// + public int? HSpace { get; set; } + + /// + /// Gets or sets the number of whitespaces on the bottom and top side of the image. + /// + public int? VSpace { get; set; } + + /// + /// Gets or sets the border thickness of the image. + /// + public int? Border { get; set; } + + /// + /// Gets or sets the class of the image. + /// + public string? Class { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Route.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Route.cs new file mode 100644 index 0000000..b55f4ab --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Route.cs @@ -0,0 +1,62 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents the route information of a Sitecore layout service response. +/// +public class Route : FieldsReader +{ + /// + /// Gets or sets the name of the database. + /// + public string? DatabaseName { get; set; } + + /// + /// Gets or sets the device ID. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets the route item ID. + /// + public string? ItemId { get; set; } + + /// + /// Gets or sets the item language. + /// + public string? ItemLanguage { get; set; } + + /// + /// Gets or sets the item version. + /// + public int? ItemVersion { get; set; } + + /// + /// Gets or sets the layout ID. + /// + public string? LayoutId { get; set; } + + /// + /// Gets or sets the template ID. + /// + public string? TemplateId { get; set; } + + /// + /// Gets or sets the template name. + /// + public string? TemplateName { get; set; } + + /// + /// Gets or sets the route name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the route display name. + /// + public string? DisplayName { get; set; } + + /// + /// Gets the route placeholders. + /// + public Dictionary Placeholders { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Site.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Site.cs new file mode 100644 index 0000000..7647915 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/Site.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents site data of a Sitecore layout service response. +/// +public class Site +{ + /// + /// Gets or sets the name of the site. + /// + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/SitecoreData.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/SitecoreData.cs new file mode 100644 index 0000000..52d0af6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/SitecoreData.cs @@ -0,0 +1,24 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents the root Sitecore result object returned in a Sitecore layout service response. +/// +public class SitecoreData +{ + /// + /// Gets or sets the of the layout response. + /// + public Context? Context { get; set; } + + /// + /// Gets or sets the of the layout response. + /// + public Route? Route { get; set; } + + /// + /// Gets the presentation list of the layout response. + /// + public List Devices { get; init; } = []; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs new file mode 100644 index 0000000..1a9f00b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Represents an arbitrary field in a Sitecore layout service response +/// that contains a value that can be edited using wrapped HTML markup. +/// +/// The value type. +public class WrappedEditableField : Field, IWrappedEditableField +{ + /// + [DataMember(Name = "editableFirstPart")] + [JsonPropertyName("editableFirstPart")] + public string EditableMarkupFirst { get; set; } = string.Empty; + + /// + [DataMember(Name = "editableLastPart")] + [JsonPropertyName("editableLastPart")] + public string EditableMarkupLast { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/PageState.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/PageState.cs new file mode 100644 index 0000000..70ea45e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/PageState.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +/// +/// The Sitecore layout service response page states. +/// +public enum PageState +{ + /// + /// The normal page state. + /// + Normal, + + /// + /// The editing page state. + /// + Edit, + + /// + /// The preview page state. + /// + Preview +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponse.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponse.cs new file mode 100644 index 0000000..9f70ff1 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponse.cs @@ -0,0 +1,59 @@ +using System.Collections.ObjectModel; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +/// +/// Models a result from calling the Sitecore layout service. +/// +public class SitecoreLayoutResponse +{ + /// + /// Initializes a new instance of the class. + /// + /// /// The object. + /// /// The list of objects. + public SitecoreLayoutResponse(SitecoreLayoutRequest request, List errors) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(errors); + + Request = request; + Errors = new ReadOnlyCollection(errors); + } + + /// + /// Initializes a new instance of the class. + /// + /// The object. + public SitecoreLayoutResponse(SitecoreLayoutRequest request) + : this(request, []) + { + } + + /// + /// Gets or sets the metadata of the response. + /// + public ILookup? Metadata { get; set; } + + /// + /// Gets or sets the content of the response. + /// + public SitecoreLayoutResponseContent? Content { get; set; } + + /// + /// Gets a value indicating whether the response has errors. + /// + public bool HasErrors => Errors.Count != 0; + + /// + /// Gets the list of errors returned by the Sitecore layout service response. + /// + public IReadOnlyCollection Errors { get; } + + /// + /// Gets the original request. + /// + public SitecoreLayoutRequest Request { get; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponseContent.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponseContent.cs new file mode 100644 index 0000000..ef79e1d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/SitecoreLayoutResponseContent.cs @@ -0,0 +1,19 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +/// +/// Models the content of the response from calling the Sitecore layout service. +/// +public class SitecoreLayoutResponseContent +{ + /// + /// Gets or sets the root object. + /// + public SitecoreData? Sitecore { get; set; } + + /// + /// Gets or sets string. + /// + public string ContextRawData { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/ComponentPropertiesNames.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/ComponentPropertiesNames.cs new file mode 100644 index 0000000..a821617 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/ComponentPropertiesNames.cs @@ -0,0 +1,37 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Constants; + +/// +/// Constants relevant to Component placeholder. +/// +public static class ComponentPropertiesNames +{ + /// + /// The name of the Id attribute. + /// + public const string Id = "uid"; + + /// + /// The name of the Fields attribute. + /// + public const string Fields = "fields"; + + /// + /// The name of the Placeholders attribute. + /// + public const string Placeholders = "placeholders"; + + /// + /// The name of the DataSource attribute. + /// + public const string DataSource = "dataSource"; + + /// + /// The name of the Params attribute. + /// + public const string Params = "params"; + + /// + /// The name of the ComponentName attribute. + /// + public const string ComponentName = "componentName"; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/EditableChromePropertiesNames.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/EditableChromePropertiesNames.cs new file mode 100644 index 0000000..1b54af0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Constants/EditableChromePropertiesNames.cs @@ -0,0 +1,27 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Constants; + +/// +/// Constants relevant to EditableChrome placeholder. +/// +public static class EditableChromePropertiesNames +{ + /// + /// The name of the Name attribute. + /// + public const string Name = "name"; + + /// + /// The name of the Type attribute. + /// + public const string Type = "type"; + + /// + /// The name of the Contents attribute. + /// + public const string Contents = "contents"; + + /// + /// The name of the Attributes attribute. + /// + public const string Attributes = "attributes"; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldConverter.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldConverter.cs new file mode 100644 index 0000000..5fa6a3f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldConverter.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +/// +/// Handles conversion of a Fields. +/// +public class FieldConverter : JsonConverter +{ + /// + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(IFieldReader); + + /// + public override IFieldReader Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + + JsonDocument doc = JsonDocument.ParseValue(ref reader); + return doc.RootElement.ValueKind switch + { + JsonValueKind.Object or JsonValueKind.Array => new JsonSerializedField(doc), + _ => throw new JsonException($"Expected an array or object when deserializing a {typeof(IFieldReader)}. Found {reader.TokenType}"), + }; + } + + /// + public override void Write(Utf8JsonWriter writer, IFieldReader value, JsonSerializerOptions options) + { + // NOTE We do not need a null check for "value" since this Converter won't handle "null" + ArgumentNullException.ThrowIfNull(options); + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs new file mode 100644 index 0000000..f35a6f8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/FieldParser.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +/// +public class FieldParser : IFieldParser +{ + /// + public Dictionary ParseFields(ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + Dictionary fields = []; + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + string? key = reader.GetString(); + reader.Read(); + JsonDocument value = ParseField(ref reader); + if (key != null) + { + fields.Add(key, new JsonSerializedField(value)); + } + } + + return fields; + } + + private static JsonDocument ParseField(ref Utf8JsonReader reader) + { + JsonDocument value = JsonDocument.ParseValue(ref reader); + + return value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/IFieldParser.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/IFieldParser.cs new file mode 100644 index 0000000..a0a30b2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/IFieldParser.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +/// +/// Handles conversion of a Field. +/// +public interface IFieldParser +{ + /// + /// Reads Json and converts to . + /// + /// The reader. + /// Parsed fields. + Dictionary ParseFields(ref Utf8JsonReader reader); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/PlaceholderFeatureConverter.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/PlaceholderFeatureConverter.cs new file mode 100644 index 0000000..953e9ba --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Converter/PlaceholderFeatureConverter.cs @@ -0,0 +1,197 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Constants; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter; + +/// +/// Handles conversion of a Placeholder feature collection. +/// +/// +/// Initializes a new instance of the class. +/// +/// The field parser. +public class PlaceholderFeatureConverter(IFieldParser fieldParser) + : JsonConverter +{ + /// + public override bool CanConvert(Type typeToConvert) => typeof(Placeholder).IsAssignableFrom(typeToConvert); + + /// + public override Placeholder Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(typeToConvert); + ArgumentNullException.ThrowIfNull(options); + + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + Placeholder placeholder = []; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + placeholder.Add(GetPlaceholderFeature(ref reader, typeToConvert, options)); + } + + return placeholder; + } + + /// + public override void Write(Utf8JsonWriter writer, Placeholder value, JsonSerializerOptions options) + { + // NOTE We do not need a null check for "value" since this Converter won't handle "null" + ArgumentNullException.ThrowIfNull(options); + + writer.WriteStartArray(); + foreach (IPlaceholderFeature feature in value) + { + JsonSerializer.Serialize(writer, feature, feature.GetType(), options); + } + + writer.WriteEndArray(); + } + + private static Dictionary GetParams(ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + int startDepth = reader.CurrentDepth; + Dictionary dictionary = []; + + while (IsReadObjectAvailable(ref reader, startDepth)) + { + string? key = reader.GetString(); + + reader.Read(); + + string value = reader.GetString() ?? string.Empty; + if (key != null) + { + dictionary.Add(key, value); + } + } + + return dictionary; + } + + private static bool IsReadObjectAvailable(ref Utf8JsonReader reader, int startDepth) + { + return reader.Read() && + (reader.TokenType != JsonTokenType.EndObject || reader.CurrentDepth != startDepth); + } + + private static EditableChrome GetEditableChromeInstance(IReadOnlyDictionary properties) + { + EditableChrome editableChrome = new() + { + Name = GetPropertyByName(properties, EditableChromePropertiesNames.Name) ?? string.Empty, + Attributes = GetPropertyByName>(properties, EditableChromePropertiesNames.Attributes) ?? [], + Content = GetPropertyByName(properties, EditableChromePropertiesNames.Contents) ?? string.Empty, + Type = GetPropertyByName(properties, EditableChromePropertiesNames.Type) ?? string.Empty + }; + return editableChrome; + } + + private static Component GetComponentInstance(Dictionary properties) + { + Component component = new() + { + Id = GetPropertyByName(properties, ComponentPropertiesNames.Id) ?? string.Empty, + Fields = GetPropertyByName>(properties, ComponentPropertiesNames.Fields) ?? [], + Name = GetPropertyByName(properties, ComponentPropertiesNames.ComponentName) ?? string.Empty, + Parameters = GetPropertyByName>(properties, ComponentPropertiesNames.Params) ?? [], + Placeholders = GetPropertyByName>(properties, ComponentPropertiesNames.Placeholders) ?? [], + DataSource = GetPropertyByName(properties, ComponentPropertiesNames.DataSource) ?? string.Empty + }; + + return component; + } + + private static TProperty? GetPropertyByName(IReadOnlyDictionary properties, string property) + where TProperty : class + { + if (properties.TryGetValue(property, out object? obj)) + { + return (TProperty)obj; + } + + return null; + } + + private IPlaceholderFeature GetPlaceholderFeature(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + int startDepth = reader.CurrentDepth; + Dictionary properties = []; + + while (IsReadObjectAvailable(ref reader, startDepth)) + { + string? propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case ComponentPropertiesNames.Fields: + properties.TryAdd(propertyName, fieldParser.ParseFields(ref reader)); + break; + case ComponentPropertiesNames.Placeholders: + properties.TryAdd(propertyName, GetPlaceholders(ref reader, typeToConvert, options)); + break; + case ComponentPropertiesNames.Params: + case EditableChromePropertiesNames.Attributes: + properties.TryAdd(propertyName, GetParams(ref reader)); + break; + case ComponentPropertiesNames.Id: + case ComponentPropertiesNames.DataSource: + case ComponentPropertiesNames.ComponentName: + case EditableChromePropertiesNames.Name: + case EditableChromePropertiesNames.Contents: + case EditableChromePropertiesNames.Type: + properties.TryAdd(propertyName, reader.GetString() ?? string.Empty); + break; + } + } + + if (properties.TryGetValue(LayoutServiceClientConstants.SitecoreChromes.ChromeTypeName, out object? type) && + type.ToString() == LayoutServiceClientConstants.SitecoreChromes.ChromeTypeValue) + { + return GetEditableChromeInstance(properties); + } + + return GetComponentInstance(properties); + } + + private Dictionary GetPlaceholders(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + int startDepth = reader.CurrentDepth; + Dictionary placeHolderDictionary = []; + + while (IsReadObjectAvailable(ref reader, startDepth)) + { + string? key = reader.GetString(); + reader.Read(); + Placeholder placeHolder = Read(ref reader, typeToConvert, options); + if (key != null) + { + placeHolderDictionary.Add(key, placeHolder); + } + } + + return placeHolderDictionary; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Fields/JsonSerializedField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Fields/JsonSerializedField.cs new file mode 100644 index 0000000..ec4e340 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/Fields/JsonSerializedField.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; + +/// +/// Encapsulates a for later deserialization as a . +/// +/// +/// Initializes a new instance of the class. +/// +/// The instance of for later deserialization. +public class JsonSerializedField(JsonDocument doc) + : FieldReader +{ + private static readonly JsonSerializerOptions Options = new JsonSerializerOptions().AddLayoutServiceDefaults(); + + private readonly string _json = doc != null ? doc.RootElement.GetRawText() : throw new ArgumentNullException(nameof(doc)); + + /// + protected override object? HandleRead(Type type) + { + // NOTE The JsonSerializerOptions used to be delivered through the deserialization but are now locked here inside the class because + // the caches inside appear to be incompatible if the options instance was used for different deserialization outside the library before. + // We should test when .NET8+ releases whether this is fixed and whether we can use the options from the deserialization again. + return JsonSerializer.Deserialize(_json, type, Options); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/ISitecoreLayoutSerializer.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/ISitecoreLayoutSerializer.cs new file mode 100644 index 0000000..f9ae596 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/ISitecoreLayoutSerializer.cs @@ -0,0 +1,16 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +/// +/// Contract that supports serialization for the Sitecore layout service. +/// +public interface ISitecoreLayoutSerializer +{ + /// + /// Deserializes the given data to a . + /// + /// The data to deserialize. + /// The deserialized . + SitecoreLayoutResponseContent? Deserialize(string data); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/JsonLayoutServiceSerializer.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/JsonLayoutServiceSerializer.cs new file mode 100644 index 0000000..5655c9c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Serialization/JsonLayoutServiceSerializer.cs @@ -0,0 +1,62 @@ +using System.Text.Json; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +/// +/// +/// Initializes a new instance of the class. +/// +public class JsonLayoutServiceSerializer : ISitecoreLayoutSerializer +{ + private static readonly JsonSerializerOptions DefaultSerializerOptions = + new JsonSerializerOptions().AddLayoutServiceDefaults(); + + private readonly JsonSerializerOptions? _serializerOptions; + + /// + /// Initializes a new instance of the class. + /// + public JsonLayoutServiceSerializer() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use. + public JsonLayoutServiceSerializer(JsonSerializerOptions options) + { + _serializerOptions = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the default used. + /// + /// The default instance. + public static JsonSerializerOptions GetDefaultSerializerOptions() + { + return DefaultSerializerOptions; + } + + /// + public SitecoreLayoutResponseContent? Deserialize(string data) + { + ArgumentException.ThrowIfNullOrWhiteSpace(data); + SitecoreLayoutResponseContent? layoutResponseContent = JsonSerializer.Deserialize(data, _serializerOptions ?? DefaultSerializerOptions); + + Context? scContext = layoutResponseContent?.Sitecore?.Context; + if (scContext != null && layoutResponseContent != null) + { + JsonDocument doc = JsonDocument.Parse(data); + layoutResponseContent.ContextRawData = doc.RootElement + .GetProperty(LayoutServiceClientConstants.Serialization.SitecoreDataPropertyName) + .GetProperty(LayoutServiceClientConstants.Serialization.ContextPropertyName) + .GetRawText(); + } + + return layoutResponseContent; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Sitecore.AspNetCore.SDK.LayoutService.Client.csproj b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Sitecore.AspNetCore.SDK.LayoutService.Client.csproj new file mode 100644 index 0000000..90e1d6e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Sitecore.AspNetCore.SDK.LayoutService.Client.csproj @@ -0,0 +1,41 @@ + + + + Sitecore Layout Service + .NET Client for the Sitecore Layout Service + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.LayoutService.Client.Tests + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests + + + + diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Attributes/UseSitecoreRenderingAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Attributes/UseSitecoreRenderingAttribute.cs new file mode 100644 index 0000000..c367890 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Attributes/UseSitecoreRenderingAttribute.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Attributes; + +/// +/// Injects middleware to support the Sitecore Rendering logic. +/// +/// +/// Initializes a new instance of the class. +/// +/// The type which configures the middleware for the request. +public class UseSitecoreRenderingAttribute(Type configurationType) + : MiddlewareFilterAttribute(configurationType) +{ + /// + /// Initializes a new instance of the class. + /// + public UseSitecoreRenderingAttribute() + : this(typeof(RenderingEnginePipeline)) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldAttribute.cs new file mode 100644 index 0000000..eebfabb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore field. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreComponentFieldAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the name of the field in the component field list to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutComponentFieldBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldsAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldsAttribute.cs new file mode 100644 index 0000000..8e3ad62 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentFieldsAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds all fields for a Sitecore . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreComponentFieldsAttribute : Attribute, IBindingSourceMetadata +{ + /// + public BindingSource BindingSource => new SitecoreLayoutComponentFieldsBindingSource(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentParameterAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentParameterAttribute.cs new file mode 100644 index 0000000..a45673b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentParameterAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore parameter. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreComponentParameterAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the rendering parameter name to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutComponentParameterBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentPropertyAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentPropertyAttribute.cs new file mode 100644 index 0000000..64d1ece --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreComponentPropertyAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore property. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreComponentPropertyAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the component property name to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutComponentPropertyBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextAttribute.cs new file mode 100644 index 0000000..7ef1516 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore object. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreContextAttribute : Attribute, IBindingSourceMetadata +{ + /// + public BindingSource BindingSource => new SitecoreLayoutContextBindingSource(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextPropertyAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextPropertyAttribute.cs new file mode 100644 index 0000000..4929be8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreContextPropertyAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore property. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreContextPropertyAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the name of the context property to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutContextPropertyBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreLayoutResponseAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreLayoutResponseAttribute.cs new file mode 100644 index 0000000..26afe3b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreLayoutResponseAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a object. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreLayoutResponseAttribute : Attribute, IBindingSourceMetadata +{ + /// + public BindingSource BindingSource => new SitecoreLayoutResponseBindingSource(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldAttribute.cs new file mode 100644 index 0000000..8dce83c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore field. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreRouteFieldAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the name of the field in the route field list to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutRouteFieldBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldsAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldsAttribute.cs new file mode 100644 index 0000000..83e2449 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRouteFieldsAttribute.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds all fields for a Sitecore . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreRouteFieldsAttribute : Attribute, IBindingSourceMetadata +{ + /// + public BindingSource BindingSource => new SitecoreLayoutRouteFieldsBindingSource(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRoutePropertyAttribute.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRoutePropertyAttribute.cs new file mode 100644 index 0000000..2ee54c9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Attributes/SitecoreRoutePropertyAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; + +/// +/// Binds a Sitecore property. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class SitecoreRoutePropertyAttribute : Attribute, IBindingSourceMetadata +{ + /// + /// Gets or sets the name of the route property to use for binding. + /// + public string Name { get; set; } = string.Empty; + + /// + public BindingSource BindingSource => new SitecoreLayoutRoutePropertyBindingSource(Name); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Extensions/BindingExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Extensions/BindingExtensions.cs new file mode 100644 index 0000000..6181665 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Extensions/BindingExtensions.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; + +/// +/// Extensions to support model binding for Sitecore layout data. +/// +public static class BindingExtensions +{ + /// + /// Gets the for the specified model type and binding source. + /// + /// The instance. + /// The type of the binding source. + /// The model type to bind to. + /// The instance. + public static BinderTypeModelBinder? GetModelBinder(this ModelBinderProviderContext context) + where TSource : SitecoreLayoutBindingSource + { + ArgumentNullException.ThrowIfNull(context); + + Type modelType = context.Metadata.UnderlyingOrModelType; + + if (modelType == typeof(TType) || typeof(TType).IsAssignableFrom(modelType)) + { + return new BinderTypeModelBinder(typeof(SitecoreLayoutModelBinder)); + } + + return context.GetModelBinder(); + } + + /// + /// Gets the for the specified binding source. + /// + /// The instance. + /// The type of the binding source. + /// The instance. + public static BinderTypeModelBinder? GetModelBinder(this ModelBinderProviderContext context) + where TSource : SitecoreLayoutBindingSource + { + ArgumentNullException.ThrowIfNull(context); + return context.BindingInfo.BindingSource is TSource ? new BinderTypeModelBinder(typeof(SitecoreLayoutModelBinder)) : null; + } + + /// + /// Adds the default Sitecore model binder providers to the . + /// + /// The to configure. + public static void AddSitecoreModelBinderProviders(this MvcOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + options.ModelBinderProviders.Insert(0, new SitecoreLayoutResponseModelBinderProvider()); + options.ModelBinderProviders.Insert(0, new SitecoreLayoutComponentModelBinderProvider()); + options.ModelBinderProviders.Insert(0, new SitecoreLayoutContextModelBinderProvider()); + options.ModelBinderProviders.Insert(0, new SitecoreLayoutRouteModelBinderProvider()); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/IViewModelBinder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/IViewModelBinder.cs new file mode 100644 index 0000000..fa84efa --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/IViewModelBinder.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +/// +/// Contract that allows models to be bound based on a . +/// +public interface IViewModelBinder +{ + /// + /// Creates an instance of and binds properties + /// based on the given . + /// + /// The instance type to be returned. + /// The to bind against. + /// A bound instance of or default(). + Task Bind(ViewContext viewContext) + where TModel : class, new(); + + /// + /// Binds properties on the given based on the given + /// . + /// + /// The instance type to be returned. + /// Ths instance to be bound. + /// The to bind against. + /// The instance of with its properties updated. + Task Bind(TModel model, ViewContext viewContext) + where TModel : class; + + /// + /// Binds properties on the given based on the given + /// . + /// + /// The instance type to be returned. + /// The to bind against. + /// The instance of with its properties updated. + Task Bind(Type modelType, ViewContext viewContext); + + /// + /// Binds properties on the given based on the given + /// . + /// + /// The instance type to be returned. + /// The to bind against. + /// The instance of with its properties updated. + Task Bind(object model, ViewContext viewContext); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutComponentModelBinderProvider.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutComponentModelBinderProvider.cs new file mode 100644 index 0000000..1e28e97 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutComponentModelBinderProvider.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; + +/// +/// Creates , +/// , +/// , +/// and +/// instances. +/// +public class SitecoreLayoutComponentModelBinderProvider : IModelBinderProvider +{ + /// + public IModelBinder? GetBinder(ModelBinderProviderContext context) + { + ArgumentNullException.ThrowIfNull(context); + + BinderTypeModelBinder? binder = (context.GetModelBinder() ?? + context.GetModelBinder() ?? + context.GetModelBinder()) ?? + context.GetModelBinder() ?? + context.GetModelBinder(); + + return binder; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutContextModelBinderProvider.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutContextModelBinderProvider.cs new file mode 100644 index 0000000..5e194fe --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutContextModelBinderProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; + +/// +/// Creates and instances. +/// +public class SitecoreLayoutContextModelBinderProvider : IModelBinderProvider +{ + /// + public IModelBinder? GetBinder(ModelBinderProviderContext context) + { + ArgumentNullException.ThrowIfNull(context); + + BinderTypeModelBinder? binder = context.GetModelBinder() ?? + context.GetModelBinder(); + return binder; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutResponseModelBinderProvider.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutResponseModelBinderProvider.cs new file mode 100644 index 0000000..4b8232c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutResponseModelBinderProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; + +/// +/// Creates instances. +/// +public class SitecoreLayoutResponseModelBinderProvider : IModelBinderProvider +{ + /// + public IModelBinder? GetBinder(ModelBinderProviderContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return context.GetModelBinder(); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutRouteModelBinderProvider.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutRouteModelBinderProvider.cs new file mode 100644 index 0000000..48d2843 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Providers/SitecoreLayoutRouteModelBinderProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; + +/// +/// Creates , +/// , +/// +/// and instances. +/// +public class SitecoreLayoutRouteModelBinderProvider : IModelBinderProvider +{ + /// + public IModelBinder? GetBinder(ModelBinderProviderContext context) + { + ArgumentNullException.ThrowIfNull(context); + + BinderTypeModelBinder? binder = context.GetModelBinder() ?? + context.GetModelBinder() ?? + context.GetModelBinder() ?? + context.GetModelBinder(); + + return binder; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreLayoutModelBinder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreLayoutModelBinder.cs new file mode 100644 index 0000000..d177a78 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreLayoutModelBinder.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +/// +/// Implements model binding for Sitecore layout data specified by the . +/// +/// The type of the binding source. +public class SitecoreLayoutModelBinder : IModelBinder + where T : SitecoreLayoutBindingSource +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger> _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The to use for logging. + public SitecoreLayoutModelBinder(IServiceProvider serviceProvider, ILogger> logger) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(logger); + _serviceProvider = serviceProvider; + _logger = logger; + } + + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + ArgumentNullException.ThrowIfNull(bindingContext); + T bindingSource = bindingContext.BindingSource as T ?? Activator.CreateInstance(); + ISitecoreRenderingContext? context = bindingContext.HttpContext.GetSitecoreRenderingContext(); + + using (IServiceScope scope = _serviceProvider.CreateScope()) + { + object? model = context != null ? bindingSource.GetModel(scope.ServiceProvider, bindingContext, context) : null; + + if (model != null) + { + bindingContext.ValidationState.TryAdd(model, new ValidationStateEntry { SuppressValidation = true }); + bindingContext.Result = ModelBindingResult.Success(model); + } + else + { + string componentWarning = context?.Component != null + ? $"\nComponent : {context.Component?.Name}" + : string.Empty; + _logger.LogWarning( + "\nFailed to bind {contextFieldName} to {contextModelTypeName} type.\nBinding Source : {sourceDisplayName}{componentWarning}", + bindingContext.FieldName, + bindingContext.ModelType.Name, + bindingSource.DisplayName, + componentWarning); + } + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreViewModelBinder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreViewModelBinder.cs new file mode 100644 index 0000000..634d760 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/SitecoreViewModelBinder.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +/// +internal class SitecoreViewModelBinder : IViewModelBinder +{ + /// + public virtual async Task Bind(ViewContext viewContext) + where TModel : class, new() + { + ArgumentNullException.ThrowIfNull(viewContext); + + TModel model = new(); + + await Bind(model, viewContext).ConfigureAwait(false); + + return model; + } + + /// + public async Task Bind(TModel model, ViewContext viewContext) + where TModel : class + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(viewContext); + + ISitecoreRenderingContext? context = viewContext.HttpContext.GetSitecoreRenderingContext(); + ControllerBase controller = context?.Controller ?? throw new NullReferenceException(Resources.Exception_ControllerCannotBeNull); + await controller.TryUpdateModelAsync(model).ConfigureAwait(false); + } + + /// + public async Task Bind(Type modelType, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(modelType); + ArgumentNullException.ThrowIfNull(viewContext); + + object model; + + try + { + object? modelInstance = Activator.CreateInstance(modelType); + model = modelInstance ?? new object(); + } + catch (TypeLoadException ex) + { + throw new ArgumentException(string.Format(Resources.Exception_CannotCreateModelInstance, modelType), ex); + } + + if (model == null) + { + throw new ArgumentException(string.Format(Resources.Exception_CannotCreateModelInstance, modelType)); + } + + await Bind(model, viewContext).ConfigureAwait(false); + + return model; + } + + /// + public async Task Bind(object model, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(viewContext); + + ISitecoreRenderingContext? context = viewContext.HttpContext.GetSitecoreRenderingContext(); + ControllerBase controller = context?.Controller ?? throw new NullReferenceException(Resources.Exception_ControllerCannotBeNull); + await controller.TryUpdateModelAsync(model, model.GetType(), string.Empty).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutBindingSource.cs new file mode 100644 index 0000000..f144d9e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutBindingSource.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Utilities for binding Sitecore layout data. +/// +/// +/// Initializes a new instance of the class. +/// +/// The binding source ID. +/// The display name. +/// A value indicating whether the source is greedy. +/// A value indicating whether the data comes from the HTTP request. +public abstract class SitecoreLayoutBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) + : BindingSource(id, displayName, isGreedy, isFromRequest) +{ + /// + /// Gets or sets the binding source name. + /// + public string? Name { get; set; } + + /// + /// Gets the model for binding. + /// + /// The . + /// The . + /// The . + /// The bound model. + public abstract object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context); + + /// + /// Gets the value of an object's property. + /// + /// The . + /// The source object to process. + /// The type of the source object. + /// A property object. + protected object? GetPropertyModel(ModelBindingContext bindingContext, T? source) + where T : class + { + if (source == null) + { + return null; + } + + string? propertyName = !string.IsNullOrWhiteSpace(Name) ? Name : bindingContext.FieldName; + + if (string.IsNullOrWhiteSpace(propertyName)) + { + return null; + } + + PropertyInfo? property = source.GetType().GetProperty(propertyName); + if (property != null && (bindingContext.ModelType == property.PropertyType || bindingContext.ModelType == typeof(object))) + { + return property.GetValue(source, null); + } + + return null; + } + + /// + /// Get the value of an object's field. + /// + /// The . + /// The source object to process. + /// The route for the current page. + /// The type of the source object. + /// A field object. + protected object? GetFieldModel(ModelBindingContext bindingContext, T? source, Route? currentRoute) + where T : IFieldsReader + { + if (source == null) + { + return null; + } + + string? fieldName = !string.IsNullOrWhiteSpace(Name) ? Name : bindingContext.FieldName; + + if (string.IsNullOrWhiteSpace(fieldName)) + { + return null; + } + + if (source.TryReadField(bindingContext.ModelMetadata.ModelType, fieldName, out object? field)) + { + return field; + } + + if (currentRoute != null && currentRoute.TryReadField(bindingContext.ModelMetadata.ModelType, fieldName, out object? routeField)) + { + return routeField; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentBindingSource.cs new file mode 100644 index 0000000..e4de26a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentBindingSource.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore data. +/// +public class SitecoreLayoutComponentBindingSource : SitecoreLayoutBindingSource +{ + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutComponentBindingSource() + : base(nameof(Component), nameof(Component), false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Type modelType = bindingContext.ModelMetadata.ModelType; + + if (context.Component != null && modelType == typeof(Component)) + { + return context.Component; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldBindingSource.cs new file mode 100644 index 0000000..b450ad2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldBindingSource.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore field data. +/// +/// +/// Initializes a new instance of the class. +/// +/// The binding source ID. +/// The display name. +/// A value indicating whether the source is greedy. +/// A value indicating whether the data comes from the HTTP request. +public class SitecoreLayoutComponentFieldBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) + : SitecoreLayoutBindingSource(id, displayName, isGreedy, isFromRequest) +{ + private const string BindingSourceId = nameof(Component) + "Field"; + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutComponentFieldBindingSource() + : this(BindingSourceId, BindingSourceId, false, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the field in the Sitecore component to use for binding. + public SitecoreLayoutComponentFieldBindingSource(string name) + : this(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Route? route = context.Response?.Content?.Sitecore?.Route; + + return context.Component != null ? GetFieldModel(bindingContext, context.Component, route) : null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldsBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldsBindingSource.cs new file mode 100644 index 0000000..fc7c94a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentFieldsBindingSource.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore fields. +/// +public class SitecoreLayoutComponentFieldsBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Component) + "Fields"; + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutComponentFieldsBindingSource() + : base(nameof(BindingSourceId), nameof(BindingSourceId), false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Type modelType = bindingContext.ModelMetadata.ModelType; + + if (context.Component != null && context.Component.TryReadFields(modelType, out object? result)) + { + return result; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentParameterBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentParameterBindingSource.cs new file mode 100644 index 0000000..5c5f4ea --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentParameterBindingSource.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore parameter data. +/// +public class SitecoreLayoutComponentParameterBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Component) + "Parameter"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the parameter in the Sitecore component to use for binding. + public SitecoreLayoutComponentParameterBindingSource(string name) + : base(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Dictionary? parameters = context.Component?.Parameters; + if (parameters == null || parameters.Count == 0) + { + return null; + } + + string? propertyName = !string.IsNullOrWhiteSpace(Name) + ? Name + : bindingContext.FieldName; + + return string.IsNullOrWhiteSpace(propertyName) + ? null + : parameters.FirstOrDefault(p => string.Equals(p.Key, propertyName, StringComparison.OrdinalIgnoreCase)).Value; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentPropertyBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentPropertyBindingSource.cs new file mode 100644 index 0000000..219396b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutComponentPropertyBindingSource.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore property data. +/// +public class SitecoreLayoutComponentPropertyBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Component) + "Property"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property in the Sitecore component to use for binding. + public SitecoreLayoutComponentPropertyBindingSource(string name) + : base(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + return context.Component != null ? GetPropertyModel(bindingContext, context.Component) : null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextBindingSource.cs new file mode 100644 index 0000000..698c06b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextBindingSource.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore data. +/// +public class SitecoreLayoutContextBindingSource : SitecoreLayoutBindingSource +{ + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutContextBindingSource() + : base(nameof(Context), nameof(Context), false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext renderingContext) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(renderingContext); + + string? rawData = renderingContext.Response?.Content?.ContextRawData; + if (rawData == null) + { + return null; + } + + JsonDocument doc = JsonDocument.Parse(rawData); + JsonProperty prop = doc.RootElement + .EnumerateObject() + .FirstOrDefault(p => p.Name.Equals(bindingContext.FieldName, StringComparison.InvariantCultureIgnoreCase)); + if (prop.Value.ValueKind != JsonValueKind.Undefined) + { + string data = prop.Value.ToString(); + if (!string.IsNullOrWhiteSpace(data)) + { + return JsonSerializer.Deserialize(data, bindingContext.ModelMetadata.ModelType, JsonLayoutServiceSerializer.GetDefaultSerializerOptions()); + } + } + + return JsonSerializer.Deserialize(rawData, bindingContext.ModelMetadata.ModelType, JsonLayoutServiceSerializer.GetDefaultSerializerOptions()); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextPropertyBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextPropertyBindingSource.cs new file mode 100644 index 0000000..475b031 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutContextPropertyBindingSource.cs @@ -0,0 +1,62 @@ +using System.ComponentModel; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore property data. +/// +public class SitecoreLayoutContextPropertyBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Context) + "Property"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property in the Sitecore context to use for binding. + public SitecoreLayoutContextPropertyBindingSource(string name) + : base(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext renderingContext) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(renderingContext); + + Context? scContext = renderingContext.Response?.Content?.Sitecore?.Context; + + object? result = GetPropertyModel(bindingContext, scContext!); + if (result != null) + { + return result; + } + + string? innerObjectData = null; + if (!string.IsNullOrWhiteSpace(renderingContext.Response?.Content?.ContextRawData)) + { + JsonDocument doc = JsonDocument.Parse(renderingContext.Response?.Content?.ContextRawData!); + JsonProperty prop = doc.RootElement + .EnumerateObject() + .FirstOrDefault(p => p.Name.Equals(bindingContext.FieldName, StringComparison.InvariantCultureIgnoreCase)); + if (prop.Value.ValueKind != JsonValueKind.Undefined) + { + innerObjectData = prop.Value.ToString(); + } + } + + if (!string.IsNullOrWhiteSpace(innerObjectData)) + { + TypeConverter converter = TypeDescriptor.GetConverter(bindingContext.ModelType); + return converter.ConvertFrom(innerObjectData); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutResponseBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutResponseBindingSource.cs new file mode 100644 index 0000000..4170706 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutResponseBindingSource.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding data. +/// +public class SitecoreLayoutResponseBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = "LayoutServiceResponse"; + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutResponseBindingSource() + : base(BindingSourceId, BindingSourceId, false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Type modelType = bindingContext.ModelMetadata.ModelType; + + if (modelType == typeof(SitecoreLayoutResponse)) + { + return context.Response; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteBindingSource.cs new file mode 100644 index 0000000..9f2b65a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteBindingSource.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore data. +/// +public class SitecoreLayoutRouteBindingSource : SitecoreLayoutBindingSource +{ + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutRouteBindingSource() + : base(nameof(Route), nameof(Route), false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Type modelType = bindingContext.ModelMetadata.ModelType; + Route? route = context.Response?.Content?.Sitecore?.Route; + + if (route != null && modelType == typeof(Route)) + { + return route; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldBindingSource.cs new file mode 100644 index 0000000..795640f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldBindingSource.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore field data. +/// +/// +/// Initializes a new instance of the class. +/// +/// The binding source ID. +/// The display name. +/// A value indicating whether the source is greedy. +/// A value indicating whether the data comes from the HTTP request. +public class SitecoreLayoutRouteFieldBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) + : SitecoreLayoutBindingSource(id, displayName, isGreedy, isFromRequest) +{ + private const string BindingSourceId = nameof(Route) + "Field"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the field in the Sitecore route to use for binding. + public SitecoreLayoutRouteFieldBindingSource(string name) + : this(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Route? route = context.Response?.Content?.Sitecore?.Route; + + return route != null ? GetFieldModel(bindingContext, route, null) : null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldsBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldsBindingSource.cs new file mode 100644 index 0000000..fd12450 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRouteFieldsBindingSource.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore fields. +/// +public class SitecoreLayoutRouteFieldsBindingSource : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Route) + "Fields"; + + /// + /// Initializes a new instance of the class. + /// + public SitecoreLayoutRouteFieldsBindingSource() + : base(nameof(BindingSourceId), nameof(BindingSourceId), false, false) + { + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Type modelType = bindingContext.ModelMetadata.ModelType; + Route? route = context.Response?.Content?.Sitecore?.Route; + + if (route != null && route.TryReadFields(modelType, out object? result)) + { + return result; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRoutePropertyBindingSource.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRoutePropertyBindingSource.cs new file mode 100644 index 0000000..dfcdf13 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Binding/Sources/SitecoreLayoutRoutePropertyBindingSource.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; + +/// +/// Binding source for binding Sitecore property data. +/// +public class SitecoreLayoutRoutePropertyBindingSource + : SitecoreLayoutBindingSource +{ + private const string BindingSourceId = nameof(Route) + "Property"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property in the Sitecore route to use for binding. + public SitecoreLayoutRoutePropertyBindingSource(string name) + : base(BindingSourceId, BindingSourceId, false, false) + { + Name = name; + } + + /// + public override object? GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(bindingContext); + ArgumentNullException.ThrowIfNull(context); + + Route? route = context.Response?.Content?.Sitecore?.Route; + + return route != null ? GetPropertyModel(bindingContext, route) : null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/ForwardHeadersOptions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/ForwardHeadersOptions.cs new file mode 100644 index 0000000..e96976f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/ForwardHeadersOptions.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +/// +/// Options to configure headers forwarding. +/// +public class ForwardHeadersOptions +{ + /// + /// Gets whitelist of headers allowed to be transferred to Layout Service request. + /// + public HashSet HeadersWhitelist { get; } = []; + + /// + /// Gets filters, which will be applied sequentially during copying of headers from request to Layout service request. + /// + public IList>> RequestHeadersFilters { get; } = []; + + /// + /// Gets filters, which will be applied sequentially during copying of headers from Layout service response to response sent by rendering host. + /// + public IList, IDictionary>> ResponseHeadersFilters { get; } = []; + + /// + /// Gets or sets the XForwardedProto header key to push the request schema under for the request when forwarding. + /// + public string XForwardedProtoHeader { get; set; } = "X-Forwarded-Proto"; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/MultisiteOptions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/MultisiteOptions.cs new file mode 100644 index 0000000..f00170c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/MultisiteOptions.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +/// +/// SitecoreRewriteOptions class represents options for SitecoreRewrite form appsettings.json file. +/// +public class MultisiteOptions +{ + /// + /// Gets Multisite options name. + /// + public const string Name = "Multisite"; + + /// + /// Gets or sets cache timeout in seconds for site resolving, since it is heavy operation. + /// + public int ResolvingCacheTimeout { get; set; } = 300; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineMarkerService.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineMarkerService.cs new file mode 100644 index 0000000..1e924e4 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineMarkerService.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +/// +/// Marker service used to identify when the Sitecore Rendering Engine services have been registered. +/// +internal class RenderingEngineMarkerService; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineOptions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineOptions.cs new file mode 100644 index 0000000..b25faa5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/RenderingEngineOptions.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +/// +/// The options to configure the Sitecore Rendering Engine. +/// +public class RenderingEngineOptions +{ +#pragma warning disable SA1513 // Closing brace should be followed by blank line + /// + /// Gets or sets the action list to configure the handler for incoming HTTP requests. + /// + public ICollection> RequestMappings { get; set; } = + [ + (http, sc) => + { + sc.Path($"/{GetRequestPath(http)}"); + + IRequestCultureFeature? feature = http.HttpContext.Features.Get(); + if (feature?.RequestCulture.Culture != null) + { + sc.Language(feature.RequestCulture.Culture.Name); + } + } + ]; +#pragma warning restore SA1513 // Closing brace should be followed by blank line + + /// + /// Gets or sets the list of objects for handling component rendering. + /// + public SortedList RendererRegistry { get; set; } = []; + + /// + /// Gets or sets the default object for handling component rendering. + /// + public ComponentRendererDescriptor? DefaultRenderer { get; set; } + + /// + /// Gets collection of actions to be executed right after RenderingEngine middleware logic. + /// + public ICollection> PostRenderingActions { get; } = []; + + private static string GetRequestPath(HttpRequest http) + { + string? path; + + if (http.RouteValues.ContainsKey(RenderingEngineConstants.RouteValues.SitecoreRoute)) + { + path = http.RouteValues[RenderingEngineConstants.RouteValues.SitecoreRoute] != null ? + http.RouteValues[RenderingEngineConstants.RouteValues.SitecoreRoute]!.ToString() : + string.Empty; + } + else + { + path = http.Path.Value; + } + + return path != null ? path.TrimStart('/') : string.Empty; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/SitecoreRenderingEngineBuilder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/SitecoreRenderingEngineBuilder.cs new file mode 100644 index 0000000..ddb1a9e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Configuration/SitecoreRenderingEngineBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +/// +/// +/// Initializes a new instance of the class. +/// +/// The initial . +public class SitecoreRenderingEngineBuilder(IServiceCollection services) + : ISitecoreRenderingEngineBuilder +{ + /// + public IServiceCollection Services { get; } = services ?? throw new ArgumentNullException(nameof(services)); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ApplicationBuilderExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..b953443 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extension methods to support the Microsoft . +/// +public static class ApplicationBuilderExtensions +{ + /// + /// Registers the Sitecore Rendering Engine middleware into the . + /// + /// The instance of the to extend. + /// The so that additional calls can be chained. + public static IApplicationBuilder UseSitecoreRenderingEngine(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + _ = app.ApplicationServices.GetService(typeof(RenderingEngineMarkerService)) ?? + throw new InvalidOperationException(Resources.Exception_SitecoreRenderingEngineServicesNotRegistered); + app.UseMiddleware(); + + return app; + } + + /// + /// Registers the to action mapping into the object. + /// + /// The instance of the to extend. + /// The mapping action to be added into the options. + internal static void AddRenderingEngineMapping(this IApplicationBuilder app, Action mapAction) + { + IOptions options = app.ApplicationServices.GetRequiredService>(); + + options.Value.MapToRequest(mapAction); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ControllerEndpointExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ControllerEndpointExtensions.cs new file mode 100644 index 0000000..823cb88 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ControllerEndpointExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Contains extension methods for using Controllers with . +/// +public static class ControllerEndpointExtensions +{ + private const string Pattern = $"{{{RenderingEngineConstants.SitecoreLocalization.RequestCulturePrefix}:{RenderingEngineConstants.SitecoreLocalization.RequestCulturePrefix}}}"; + + /// + /// Add default endpoint which supports language embedded prefixes. Like Http://localhost/da-DK/home. + /// + /// parameter. + /// Name of the route. + /// Default action. + /// Default controller. + /// A for endpoints associated with controller actions for this route. + public static IEndpointConventionBuilder MapSitecoreLocalizedRoute(this IEndpointRouteBuilder endpoints, string routeName, string action, string controller) + { + ArgumentNullException.ThrowIfNull(routeName); + ArgumentNullException.ThrowIfNull(controller); + ArgumentException.ThrowIfNullOrWhiteSpace(action); + + return endpoints.MapControllerRoute( + name: routeName, + pattern: $"/{Pattern}/{{**{RenderingEngineConstants.RouteValues.SitecoreRoute}}}", + defaults: new { controller, action }); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/EncodingExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/EncodingExtensions.cs new file mode 100644 index 0000000..d77a0e7 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/EncodingExtensions.cs @@ -0,0 +1,53 @@ +using System.Text; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extension methods to support custom encoding / decoding. +/// +internal static class EncodingExtensions +{ + private static readonly string[] HeaderEncodingTable = + [ + "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", + "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f", + "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", + "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f" + ]; + + /// + /// Encode the following characters in the provided value: + /// - All CTL characters except HT (horizontal tab) + /// - DEL character (\x7f) + /// This is useful in preventing CRLF Injection attacks. + /// Utility method based on: https://referencesource.microsoft.com/#System.Web/Util/HttpEncoder.cs,b5c8b7b5bb004908,references. + /// + /// The string value to encode. + /// The encoded value. + public static string EncodeControlCharacters(this string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + StringBuilder sanitizedHeader = new(); + foreach (char c in value) + { + if (c < 32 && c != 9) + { + sanitizedHeader.Append(HeaderEncodingTable[c]); + } + else if (c == 127) + { + sanitizedHeader.Append("%7f"); + } + else + { + sanitizedHeader.Append(c); + } + } + + return sanitizedHeader.ToString(); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ForwardHeadersMarkerService.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ForwardHeadersMarkerService.cs new file mode 100644 index 0000000..80cc26d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ForwardHeadersMarkerService.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Marker for Forward Headers. +/// +internal class ForwardHeadersMarkerService; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpContextExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..0008d76 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpContextExtensions.cs @@ -0,0 +1,160 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Http context extensions. +/// +public static class HttpContextExtensions +{ + /// + /// Extension method for copying of Http headers. + /// + /// Source collection. + /// Header name to copy. + /// Destination collection. + public static void CopyHeader(this IEnumerable> source, string headerKey, IDictionary destination) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentException.ThrowIfNullOrWhiteSpace(headerKey); + ArgumentNullException.ThrowIfNull(destination); + + foreach (KeyValuePair keyValuePair in source) + { + if (keyValuePair.Key.Equals(headerKey, StringComparison.OrdinalIgnoreCase)) + { + destination.Add(keyValuePair.Key, keyValuePair.Value!); + break; + } + } + } + + /// + /// Extension method for copying of Http headers. + /// + /// Source collection. + /// Header name to copy. + /// Destination collection. + public static void CopyHeader(this ILookup source, string headerKey, IDictionary destination) + { + // NOTE Other parameters are validated in the called method. + ArgumentNullException.ThrowIfNull(source); + + IEnumerable> modifiedSource = source.Select(group => + new KeyValuePair(group.Key, new StringValues(group.ToArray()))); + + CopyHeader(modifiedSource, headerKey, destination); + } + + /// + /// Extension method for collection manipulation simplifications. It inserts/appends string value to dictionary item. + /// Is useful for headers manipulations. + /// + /// Destination collection. + /// Key name. + /// Value to be added to values array. + public static void AppendValue(this IDictionary collection, string key, string value) + { + ArgumentNullException.ThrowIfNull(collection); + ArgumentException.ThrowIfNullOrWhiteSpace(key); + ArgumentException.ThrowIfNullOrWhiteSpace(value); + + string[] valueToAdd = + [ + value + ]; + + if (collection.TryAdd(key, valueToAdd)) + { + return; + } + + string[] values = collection[key]; + collection[key] = [.. values, .. valueToAdd]; + } + + /// + /// Updates response with metadata from sitecore rendering context. + /// + /// Sitecore rendering context. + /// Http context. + public static void UpdateResponseWithLayoutMetadata(this ISitecoreRenderingContext renderingContext, HttpContext context) + { + ArgumentNullException.ThrowIfNull(renderingContext); + ArgumentNullException.ThrowIfNull(context); + + ForwardHeadersOptions options = context.RequestServices.GetRequiredService>().Value; + + ILookup? metadata = renderingContext.Response?.Metadata; + + if (metadata != null) + { + Dictionary filteredHeaders = []; + + foreach (Action, IDictionary> filter in options.ResponseHeadersFilters) + { + filter(metadata, filteredHeaders); + } + + foreach (KeyValuePair meta in filteredHeaders) + { + context.Response.Headers.Append(meta.Key, meta.Value); + } + } + } + + /// + /// Sets request resolved site name globally for using it between middlewares. + /// + /// Http context. + /// Sitecore rendering context. + public static void SetResolvedSiteName(this HttpContext context, string siteName) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(siteName); + context.Items.TryAdd("resolvedSiteName", siteName); + } + + /// + /// Try get request resolved site name. + /// + /// Http context. + /// Resolved site name out param. + /// Result boolean value. + public static bool TryGetResolvedSiteName(this HttpContext context, out string? resolvedSiteName) + { + ArgumentNullException.ThrowIfNull(context); + bool result = context.Items.TryGetValue("resolvedSiteName", out object? resolvedSiteNameObject); + resolvedSiteName = resolvedSiteNameObject?.ToString(); + + return result; + } + + /// + /// Gets the from the . + /// + /// The instance to retrieve the Sitecore rendering context from. + /// The instance. + public static ISitecoreRenderingContext? GetSitecoreRenderingContext(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + return context.Features.Get(); + } + + /// + /// Sets the in the current collection. + /// + /// The current . + /// The to save in the feature collection. + public static void SetSitecoreRenderingContext(this HttpContext context, ISitecoreRenderingContext renderingContext) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(renderingContext); + context.Features.Set(renderingContext); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpRequestExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpRequestExtensions.cs new file mode 100644 index 0000000..1180f71 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/HttpRequestExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extension methods for the . +/// +internal static class HttpRequestExtensions +{ + /// + /// Checks that value for the specified key present in the query string or cookies. + /// + /// The instance. + /// The key for the request value to get. + /// A query string value if the key is matched in the query string, otherwise a cookie value if the key is matched in the cookies, otherwise null. + /// true if value exist in query string or cookies, otherwise false. + public static bool TryGetValueFromQueryOrCookies(this HttpRequest httpRequest, string key, out string? value) + { + ArgumentNullException.ThrowIfNull(httpRequest); + ArgumentNullException.ThrowIfNull(key); + + value = GetValueFromQueryOrCookies(httpRequest, key); + + return value != null; + } + + /// + /// Gets the value for the specified key from the query string or cookies. + /// + /// The instance. + /// The key for the request value to get. + /// A query string value if the key is matched in the query string, otherwise a cookie value if the key is matched in the cookies, otherwise null. + public static string? GetValueFromQueryOrCookies(this HttpRequest httpRequest, string key) + { + ArgumentNullException.ThrowIfNull(httpRequest); + ArgumentNullException.ThrowIfNull(key); + + StringValues queryValue = httpRequest.Query[key]; + if (!string.IsNullOrEmpty(queryValue)) + { + return queryValue; + } + + string? cookieValue = httpRequest.Cookies[key]; + if (!string.IsNullOrEmpty(cookieValue)) + { + return cookieValue; + } + + return null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs new file mode 100644 index 0000000..e77e4d3 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Multisite app configuration. +/// +public static class MultisiteAppConfigurationExtensions +{ + /// + /// Configures multisite. In case of global configuration of RenderingEngine. + /// + /// Application builder. + /// Modified application builder. + public static IApplicationBuilder UseMultisite(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + ISiteResolver? siteResolverService = app.ApplicationServices.GetService(); + ISiteCollectionService? siteCollectionService = app.ApplicationServices.GetService(); + + if (siteResolverService == null || siteCollectionService == null) + { + return app; + } + + return app.UseMiddleware(); + } + + /// + /// Configuration of multisite functionality. + /// + /// The to add services to. + /// Multisite middleware options configuration. + /// The so that additional calls can be chained. + public static IServiceCollection AddMultisite(this IServiceCollection services, Action? configuration = null) + { + ArgumentNullException.ThrowIfNull(services); + + if (configuration != null) + { + services.Configure(configuration); + } + + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RazorPageExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RazorPageExtensions.cs new file mode 100644 index 0000000..1a18988 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RazorPageExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc.Razor; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extensions to support Razor pages and views. +/// +public static class RazorPageExtensions +{ + /// + /// Gets the current Sitecore data. + /// + /// The current . + /// The current instance of . + public static Route? SitecoreRoute(this IRazorPage page) + { + ArgumentNullException.ThrowIfNull(page); + + Route? data = page.ViewContext.HttpContext.GetSitecoreRenderingContext()?.Response?.Content?.Sitecore?.Route; + return data ?? default; + } + + /// + /// Gets the current Sitecore . + /// + /// The current . + /// The current instance of . + public static Context? SitecoreContext(this IRazorPage page) + { + ArgumentNullException.ThrowIfNull(page); + + Context? data = page.ViewContext.HttpContext.GetSitecoreRenderingContext()?.Response?.Content?.Sitecore?.Context; + return data ?? default; + } + + /// + /// Gets the current Sitecore . + /// + /// The current . + /// The current instance of . + public static Component? SitecoreComponent(this IRazorPage page) + { + ArgumentNullException.ThrowIfNull(page); + + return page.ViewContext.HttpContext.GetSitecoreRenderingContext()?.Component; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs new file mode 100644 index 0000000..ea0c681 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs @@ -0,0 +1,306 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extensions to help configure . +/// +public static class RenderingEngineOptionsExtensions +{ + /// + /// Maps a Sitecore layout component name to a partial view rendering. The view will receive a model. + /// + /// The to configure. + /// The path of the partial view. The file name of the partial view will be the registered Sitecore layout component name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddPartialView( + this RenderingEngineOptions options, + string partialViewPath) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(partialViewPath); + + string layoutComponentName = ExtractComponentNameFromViewPath(partialViewPath); + + return AddPartialView( + options, + layoutComponentName, + partialViewPath); + } + + /// + /// Maps a Sitecore layout component name to a partial view rendering. The view will receive a model. + /// + /// The to configure. + /// The name of the layout component. + /// The path of the partial view. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddPartialView( + this RenderingEngineOptions options, + string layoutComponentName, + string partialViewPath) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(layoutComponentName); + ArgumentException.ThrowIfNullOrWhiteSpace(partialViewPath); + + return AddPartialView( + options, + name => layoutComponentName.Equals(name, StringComparison.OrdinalIgnoreCase), + partialViewPath); + } + + /// + /// Maps a Sitecore layout component name to a partial view rendering. The view will receive a model. + /// + /// The to configure. + /// The predicate to use when attempting to match a layout component. + /// The path of the partial view. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddPartialView( + this RenderingEngineOptions options, + Predicate match, + string partialViewPath) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(match); + ArgumentException.ThrowIfNullOrWhiteSpace(partialViewPath); + + ComponentRendererDescriptor descriptor = PartialViewComponentRenderer.Describe(match, partialViewPath); + + options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); + + return options; + } + + /// + /// Maps any unmatched Sitecore layout component to a default partial view. + /// This provides a visual appearance when the Sitecore layout service returns a component name that no implementation exists for. + /// The view will receive a model. + /// Ensure this is registered last as this mapping will override any subsequent component registrations. + /// + /// The to configure. + /// The path of the partial view. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddDefaultPartialView( + this RenderingEngineOptions options, + string partialViewPath) + { + return AddPartialView(options, _ => true, partialViewPath); + } + + /// + /// Maps a Sitecore layout component name to a view component rendering. + /// + /// The to configure. + /// The view component name. This is also used as the layout component registration name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddViewComponent( + this RenderingEngineOptions options, + string viewComponentName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); + + return AddViewComponent( + options, + viewComponentName, + viewComponentName); + } + + /// + /// Maps a Sitecore layout component name to a view component rendering. + /// + /// The to configure. + /// The name of the layout component. + /// The view component name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddViewComponent( + this RenderingEngineOptions options, + string layoutComponentName, + string viewComponentName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(layoutComponentName); + ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); + + return AddViewComponent( + options, + name => layoutComponentName.Equals(name, StringComparison.OrdinalIgnoreCase), + viewComponentName); + } + + /// + /// Maps a Sitecore layout component name to a view component rendering. + /// + /// The to configure. + /// A predicate to use when attempting to match a layout component. + /// The view component name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddViewComponent( + this RenderingEngineOptions options, + Predicate match, + string viewComponentName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(match); + ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); + + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName); + + options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); + + return options; + } + + /// + /// Maps a Sitecore layout component name to a partial view rendering, using the default Sitecore view component to model bind it. + /// + /// The model type to use for view binding. + /// The to configure. + /// The view name and layout component name. If the view name is a full path, the view's file name will be the layout component name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddModelBoundView( + this RenderingEngineOptions options, + string viewName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(viewName); + + return AddModelBoundView( + options, + ExtractComponentNameFromViewPath(viewName), + viewName); + } + + /// + /// Maps a Sitecore layout component name to a partial view rendering, using the default Sitecore view component to model bind it. + /// + /// The model type to use for view binding. + /// The to configure. + /// The name of the layout component. + /// The view name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddModelBoundView( + this RenderingEngineOptions options, + string layoutComponentName, + string viewName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(layoutComponentName); + ArgumentException.ThrowIfNullOrWhiteSpace(viewName); + + return AddModelBoundView( + options, + match => match.Equals(layoutComponentName, StringComparison.OrdinalIgnoreCase), + viewName); + } + + /// + /// Maps a Sitecore layout component name to a partial view rendering, using the default Sitecore view component to model bind it. + /// + /// The model type to use for view binding. + /// The to configure. + /// A predicate to use when attempting to match a layout component. + /// The view name. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddModelBoundView( + this RenderingEngineOptions options, + Predicate match, + string viewName) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(match); + ArgumentException.ThrowIfNullOrWhiteSpace(viewName); + + ComponentRendererDescriptor descriptor = new( + match, + sp => ActivatorUtilities.CreateInstance>( + sp, + RenderingEngineConstants.SitecoreViewComponents.DefaultSitecoreViewComponentName, + viewName)); + + options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); + + return options; + } + + /// + /// Maps a default . + /// + /// The to use for the default renderer. + /// The to configure. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddDefaultComponentRenderer( + this RenderingEngineOptions options) + where T : IComponentRenderer + { + ArgumentNullException.ThrowIfNull(options); + + ComponentRendererDescriptor descriptor = new(_ => true, services => ActivatorUtilities.CreateInstance(services)); + options.DefaultRenderer = descriptor; + + return options; + } + + /// + /// Maps a default . + /// + /// The to configure. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddDefaultComponentRenderer( + this RenderingEngineOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + return options.AddDefaultComponentRenderer(); + } + + /// + /// Adds mapping action. + /// + /// The to configure. + /// The mapping action to configure . + /// The so that additional calls can be chained. + public static RenderingEngineOptions MapToRequest(this RenderingEngineOptions options, Action mapAction) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(mapAction); + + options.RequestMappings.Add(mapAction); + + return options; + } + + /// + /// Adds post rendering action to be executed after Rendering engine logic. + /// + /// The to configure. + /// The action to execute after rendering engine/>. + /// The so that additional calls can be chained. + public static RenderingEngineOptions AddPostRenderingAction(this RenderingEngineOptions options, Action postAction) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(postAction); + + options.PostRenderingActions.Add(postAction); + + return options; + } + + /// + /// Extracts the final name of a path that may be full or not to a view. + /// + /// "Foo" => "Foo", "~/bar/Baz.cshtml" => "Baz". + /// The path to the partial view to resolve. + /// The file name of the partial view. + private static string ExtractComponentNameFromViewPath(string partialViewPath) + { + return Path.GetFileNameWithoutExtension(partialViewPath); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RequestLocalizationOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RequestLocalizationOptionsExtensions.cs new file mode 100644 index 0000000..6232220 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RequestLocalizationOptionsExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Localization.Routing; +using Sitecore.AspNetCore.SDK.RenderingEngine.Localization; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extensions to help configure . +/// +public static class RequestLocalizationOptionsExtensions +{ + /// + /// Adds list of to . + /// + /// The to configure. + public static void UseSitecoreRequestLocalization(this RequestLocalizationOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + options.RequestCultureProviders.Insert(0, new SitecoreQueryStringCultureProvider()); + options.RequestCultureProviders.Insert(1, new RouteDataRequestCultureProvider + { + RouteDataStringKey = RenderingEngineConstants.SitecoreLocalization.RequestCulturePrefix, + UIRouteDataStringKey = RenderingEngineConstants.SitecoreLocalization.RequestCulturePrefix + }); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ServiceCollectionExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..5636374 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,157 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Filters; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Localization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Mappers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Routing; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Extension methods for setting up Sitecore Rendering Engine related services in an . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the Sitecore Rendering Engine services to the . + /// + /// The to add services to. + /// Configures the Rendering Engine options. + /// The so that additional calls can be chained. + public static ISitecoreRenderingEngineBuilder AddSitecoreRenderingEngine( + this IServiceCollection services, + Action? options = null) + { + ArgumentNullException.ThrowIfNull(services); + + // Only register services if marker interface is missing + if (services.All(s => s.ServiceType != typeof(RenderingEngineMarkerService))) + { + // Always try to add services you don't own + services.TryAddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + + services + .AddMvc(configure => + { + configure.Filters.Add(new SitecoreLayoutContextControllerFilter()); + configure.AddSitecoreModelBinderProviders(); + }); + + services.AddLocalizationServices(); + services.AddSingleton(); + } + + if (options != null) + { + services.Configure(options); + } + + return new SitecoreRenderingEngineBuilder(services); + } + + /// + /// Enables forwarding of headers. + /// + /// The to add services to. + /// Configures the headers forwarding options. + /// The so that additional calls can be chained. + public static ISitecoreRenderingEngineBuilder ForwardHeaders(this ISitecoreRenderingEngineBuilder serviceBuilder, Action? options = null) + { + ArgumentNullException.ThrowIfNull(serviceBuilder); + + IServiceCollection services = serviceBuilder.Services; + + if (services.All(s => s.ServiceType != typeof(ForwardHeadersMarkerService))) + { + services.Configure(sitecoreTrackingOptions => + { + // Default white list of headers to transfer to LS + sitecoreTrackingOptions.RequestHeadersFilters.Add((httpRequest, resultCollection) => + { + foreach (string headerKey in sitecoreTrackingOptions.HeadersWhitelist) + { + httpRequest.Headers.CopyHeader(headerKey, resultCollection); + } + }); + + sitecoreTrackingOptions.ResponseHeadersFilters.Add((responseMetadata, resultCollection) => + { + foreach (string headerKey in sitecoreTrackingOptions.HeadersWhitelist) + { + responseMetadata.CopyHeader(headerKey, resultCollection); + } + }); + }); + + services.Configure(renderingOptions => + { + renderingOptions.MapToRequest((httpRequest, layoutRequest) => + { + ForwardHeadersOptions headersForwardingOptions = httpRequest.HttpContext.RequestServices.GetRequiredService>().Value; + + IList>> headersFilters = headersForwardingOptions.RequestHeadersFilters; + + Dictionary proxiedMetadata = new(comparer: StringComparer.OrdinalIgnoreCase); + + // Apply all filters to headers collection + foreach (Action> action in headersFilters) + { + action(httpRequest, proxiedMetadata); + } + + if (!string.IsNullOrEmpty(httpRequest.HttpContext.Request.Scheme)) + { + string scheme = httpRequest.HttpContext.Request.Scheme; + + proxiedMetadata.Add(headersForwardingOptions.XForwardedProtoHeader, [scheme]); + } + + layoutRequest.AddHeaders(proxiedMetadata); + }); + + renderingOptions.AddPostRenderingAction(httpContext => + { + ISitecoreRenderingContext? sitecoreRenderingContext = httpContext.GetSitecoreRenderingContext(); + sitecoreRenderingContext?.UpdateResponseWithLayoutMetadata(httpContext); + }); + }); + + services.AddSingleton(); + } + + if (options != null) + { + services.Configure(options); + } + + return serviceBuilder; + } + + /// + /// Adds sitecore localization services. + /// + /// The to add services to. + internal static void AddLocalizationServices(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddSingleton(); + services.Configure(options => + { + options.ConstraintMap.Add(RenderingEngineConstants.SitecoreLocalization.RequestCulturePrefix, typeof(LanguageRouteConstraint)); + }); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs new file mode 100644 index 0000000..710e258 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs @@ -0,0 +1,68 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebUtilities; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +/// +/// Set of extension methods for Sitecore fields. +/// +public static partial class SitecoreFieldExtensions +{ + /// + /// Gets modified URL string to Sitecore media item. + /// + /// The image field. + /// Image parameters, example: new { mw = 100, mh = 50 }. **IMPORTANT**: All the parameters you pass must be whitelisted for resizing to occur. See /sitecore/config/*.config (search for 'allowedMediaParams'). + /// Media item URL. + public static string? GetMediaLink(this ImageField imageField, object? imageParams) + { + ArgumentNullException.ThrowIfNull(imageField); + string? urlStr = imageField.Value.Src; + + if (urlStr == null) + { + return null; + } + + return GetSitecoreMediaUri(urlStr, imageParams); + } + + /// + /// Gets URL to Sitecore media item. + /// + /// The image URL. + /// Image parameters. + /// Media item URL. + private static string GetSitecoreMediaUri(string url, object? imageParams) + { + // TODO What's the reason we strip away existing querystring? + if (imageParams != null) + { + string[] urlParts = url.Split('?'); + if (urlParts.Length > 1) + { + url = urlParts[0]; + } + + RouteValueDictionary parameters = new(imageParams); + foreach (string key in parameters.Keys) + { + url = QueryHelpers.AddQueryString(url, key, parameters[key]?.ToString() ?? string.Empty); + } + } + + // TODO Review hardcoded matching and replacement + Match match = MediaUrlPrefixRegex().Match(url); + if (match.Success) + { + url = url.Replace(match.Value, $"/{match.Groups[1]}/jssmedia/", StringComparison.InvariantCulture); + } + + return url; + } + + [GeneratedRegex("/([-~]{1})/media/")] + private static partial Regex MediaUrlPrefixRegex(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Filters/SitecoreLayoutContextControllerFilter.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Filters/SitecoreLayoutContextControllerFilter.cs new file mode 100644 index 0000000..d4cd01b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Filters/SitecoreLayoutContextControllerFilter.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Filters; + +/// +/// Identifies the current controller for the . +/// +public class SitecoreLayoutContextControllerFilter : IActionFilter +{ + /// + public void OnActionExecuting(ActionExecutingContext context) + { + ArgumentNullException.ThrowIfNull(context); + + ISitecoreRenderingContext? renderingContext = context.HttpContext.GetSitecoreRenderingContext(); + if (renderingContext != null) + { + renderingContext.Controller = context.Controller as ControllerBase; + } + } + + /// + public void OnActionExecuted(ActionExecutedContext context) + { + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreLayoutRequestMapper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreLayoutRequestMapper.cs new file mode 100644 index 0000000..2394e1d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreLayoutRequestMapper.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +/// +/// Contract for implementing the mapping logic from a to a . +/// +public interface ISitecoreLayoutRequestMapper +{ + /// + /// Maps a to a . + /// + /// The to map. + /// A mapped . + SitecoreLayoutRequest Map(HttpRequest request); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingContext.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingContext.cs new file mode 100644 index 0000000..950129d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingContext.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +/// +/// Represents the context data for Sitecore rendering logic. +/// +public interface ISitecoreRenderingContext +{ + /// + /// Gets or sets the current . + /// + SitecoreLayoutResponse? Response { get; set; } + + /// + /// Gets or sets the current . + /// + public Component? Component { get; set; } + + /// + /// Gets or sets the current . + /// + public ControllerBase? Controller { get; set; } + + /// + /// Gets or sets the current . + /// + public RenderingHelpers? RenderingHelpers { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingEngineBuilder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingEngineBuilder.cs new file mode 100644 index 0000000..91df10f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Interfaces/ISitecoreRenderingEngineBuilder.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +/// +/// Contract for configuring Sitecore Rendering Engine services. +/// +public interface ISitecoreRenderingEngineBuilder +{ + /// + /// Gets the where Sitecore Rendering Engine services are configured. + /// + IServiceCollection Services { get; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Localization/SitecoreQueryStringCultureProvider.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Localization/SitecoreQueryStringCultureProvider.cs new file mode 100644 index 0000000..5673a87 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Localization/SitecoreQueryStringCultureProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Localization; + +/// +public class SitecoreQueryStringCultureProvider : QueryStringRequestCultureProvider +{ + /// + /// Initializes a new instance of the class. + /// + public SitecoreQueryStringCultureProvider() + { + QueryStringKey = RequestKeys.Language; + UIQueryStringKey = RequestKeys.Language; + } + + /// + public override async Task DetermineProviderCultureResult(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + ProviderCultureResult? cultureResult = await base.DetermineProviderCultureResult(httpContext).ConfigureAwait(false); + if (cultureResult != null) + { + return cultureResult; + } + + string? cookie = httpContext.Request.Cookies[RequestKeys.Language]; + + if (string.IsNullOrEmpty(cookie)) + { + return await NullProviderCultureResult.ConfigureAwait(false); + } + + return Task.FromResult(new ProviderCultureResult(new StringSegment(cookie))).Result; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Mappers/SitecoreLayoutRequestMapper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Mappers/SitecoreLayoutRequestMapper.cs new file mode 100644 index 0000000..9908870 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Mappers/SitecoreLayoutRequestMapper.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Mappers; + +/// +/// Maps a to a . +/// +public class SitecoreLayoutRequestMapper : ISitecoreLayoutRequestMapper +{ + private readonly ICollection> _handlers; + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + public SitecoreLayoutRequestMapper(IOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + _handlers = options.Value.RequestMappings; + if (_handlers == null) + { + throw new ArgumentException(Resources.Exception_RequestMappings_Required); + } + } + + /// + public SitecoreLayoutRequest Map(HttpRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + SitecoreLayoutRequest scRequest = []; + + foreach (Action handler in _handlers) + { + handler.Invoke(request, scRequest); + } + + return scRequest; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/Site.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/Site.cs new file mode 100644 index 0000000..c87f8df --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/Site.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +/// +/// Site model. +/// +internal class Site +{ + /// + /// Gets or sets the SiteInfo collection of the Site. + /// + public SiteInfo?[]? SiteInfoCollection { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfo.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfo.cs new file mode 100644 index 0000000..c68bece --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfo.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +/// +/// SiteInfo model. +/// +public class SiteInfo +{ + /// + /// Gets or sets Name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets Host Name. + /// + public string? HostName { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfoCollectionResult.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfoCollectionResult.cs new file mode 100644 index 0000000..511b1db --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/Models/SiteInfoCollectionResult.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +/// +/// SiteInfo Collection Result model. +/// +internal class SiteInfoCollectionResult +{ + /// + /// Gets or sets Site. + /// + public Site? Site { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/MultisiteMiddleware.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/MultisiteMiddleware.cs new file mode 100644 index 0000000..9f226de --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/MultisiteMiddleware.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; + +/// +/// Multisite Middleware. +/// +internal class MultisiteMiddleware +{ + private readonly RequestDelegate _next; + + private readonly ISiteResolver _siteResolver; + + private readonly IMemoryCache _memoryCache; + + private readonly MultisiteOptions _multisiteOptions; + + /// + /// Initializes a new instance of the class. + /// + /// Next middleware to run in the chain. + /// Site resolver. + /// Memory Cache. + /// Rendering Engine Options. + /// Multisite options. + /// When next, siteResolver or memoryCache arguments are null. + public MultisiteMiddleware(RequestDelegate next, ISiteResolver siteResolver, IMemoryCache memoryCache, IOptions renderingEngineOptions, IOptions multisiteOptions) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _siteResolver = siteResolver ?? throw new ArgumentNullException(nameof(siteResolver)); + _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); + _multisiteOptions = multisiteOptions != null ? multisiteOptions.Value : throw new ArgumentNullException(nameof(multisiteOptions)); + + ArgumentNullException.ThrowIfNull(renderingEngineOptions); + renderingEngineOptions.Value.MapToRequest(SiteNameChangingAction); + } + + /// + /// Execution method of the middleware. + /// + /// The Http Context. + /// A representing the result of the asynchronous operation. + public async Task Invoke(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + string hostName = httpContext.Request.Host.Value; + string? resolvedSiteName; + + // Site name can be forced by query string parameter + if (httpContext.Request.Query.TryGetValue("sc_site", out StringValues scSiteFromQuery)) + { + resolvedSiteName = scSiteFromQuery; + } + else + { + resolvedSiteName = await _memoryCache.GetOrCreateAsync($"{hostName}_siteName_key", async cacheEntry => + { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_multisiteOptions.ResolvingCacheTimeout); + + return await _siteResolver.GetByHost(httpContext.Request.Host.Value).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + if (!string.IsNullOrWhiteSpace(resolvedSiteName)) + { + httpContext.SetResolvedSiteName(resolvedSiteName); + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static void SiteNameChangingAction(HttpRequest httpRequest, SitecoreLayoutRequest layoutRequest) + { + if (httpRequest.HttpContext.TryGetResolvedSiteName(out string? resolvedSiteName)) + { + layoutRequest.SiteName(resolvedSiteName); + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEngineMiddleware.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEngineMiddleware.cs new file mode 100644 index 0000000..046fcfd --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEngineMiddleware.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; + +/// +/// The Rendering Engine middleware implementation that calls the Sitecore layout service +/// and stores the as a feature. +/// +/// +/// Initializes a new instance of the class. +/// +/// The next middleware to call. +/// The to map the HttpRequest to a Layout Service request. +/// The layout service client. +/// Rendering Engine options. +public class RenderingEngineMiddleware(RequestDelegate next, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService, IOptions options) +{ + private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next)); + + private readonly ISitecoreLayoutRequestMapper _requestMapper = requestMapper ?? throw new ArgumentNullException(nameof(requestMapper)); + + private readonly ISitecoreLayoutClient _layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); + + private readonly RenderingEngineOptions _options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + + /// + /// The middleware Invoke method. + /// + /// The current . + /// The current . + /// The current . + /// A to support async calls. + public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) + { + ArgumentNullException.ThrowIfNull(httpContext); + ArgumentNullException.ThrowIfNull(viewComponentHelper); + ArgumentNullException.ThrowIfNull(htmlHelper); + + // this protects from multiple time executions when Global and Attribute based configurations are used at the same time. + if (httpContext.Items.ContainsKey(nameof(RenderingEngineMiddleware))) + { + throw new ApplicationException(Resources.Exception_InvalidRenderingEngineConfiguration); + } + + if (httpContext.GetSitecoreRenderingContext() == null) + { + SitecoreLayoutResponse response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(false); + + SitecoreRenderingContext scContext = new() + { + Response = response, + RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) + }; + + httpContext.SetSitecoreRenderingContext(scContext); + } + else + { + ISitecoreRenderingContext? scContext = httpContext.GetSitecoreRenderingContext(); + if (scContext != null) + { + scContext.RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); + } + } + + foreach (Action action in _options.PostRenderingActions) + { + action(httpContext); + } + + httpContext.Items.Add(nameof(RenderingEngineMiddleware), null); + + await _next(httpContext).ConfigureAwait(false); + } + + private async Task GetSitecoreLayoutResponse(HttpContext httpContext) + { + SitecoreLayoutRequest request = _requestMapper.Map(httpContext.Request); + ArgumentNullException.ThrowIfNull(request); + return await _layoutService.Request(request).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEnginePipeline.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEnginePipeline.cs new file mode 100644 index 0000000..e34f1e8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Middleware/RenderingEnginePipeline.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; + +/// +/// Exposes logic to configure Rendering Engine features for a request. +/// +public class RenderingEnginePipeline +{ + /// + /// Adds the Sitecore Rendering Engine features to the given . + /// + /// The to add features to. + public virtual void Configure(IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + app.UseSitecoreRenderingEngine(); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.Designer.cs new file mode 100644 index 0000000..adfccb3 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.Designer.cs @@ -0,0 +1,288 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sitecore.AspNetCore.SDK.RenderingEngine.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unable to dynamically create an instance of '{0}'. + /// + internal static string Exception_CannotCreateModelInstance { + get { + return ResourceManager.GetString("Exception_CannotCreateModelInstance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to find appropriate 'BindView' method to execute for the model.. + /// + internal static string Exception_CannotFindBindViewMethodOnViewComponent { + get { + return ResourceManager.GetString("Exception_CannotFindBindViewMethodOnViewComponent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find a valid renderer for the type {0}.. + /// + internal static string Exception_CannotFindValidRenderer { + get { + return ResourceManager.GetString("Exception_CannotFindValidRenderer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Component with id '{0}' name cannot be null or empty.. + /// + internal static string Exception_ComponentNameNotDefined { + get { + return ResourceManager.GetString("Exception_ComponentNameNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The component renderer descriptor for {0} is null. Please ensure that correct Sitecore component-to-view mappings are defined as part of the AddSitecoreRenderingEngine options in Startup.ConfigureServices.. + /// + internal static string Exception_ComponentRendererDescriptorIsNull { + get { + return ResourceManager.GetString("Exception_ComponentRendererDescriptorIsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SitecoreRenderingContext controller cannot be null.. + /// + internal static string Exception_ControllerCannotBeNull { + get { + return ResourceManager.GetString("Exception_ControllerCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not resolve endpoint data.. + /// + internal static string Exception_CouldNotResolveEndpointData { + get { + return ResourceManager.GetString("Exception_CouldNotResolveEndpointData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HttpRequest cannot be null.. + /// + internal static string Exception_HttpRequestCannotBeNull { + get { + return ResourceManager.GetString("Exception_HttpRequestCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RenderingEngineMiddleware can only be executed once. Make sure global rendering engine configuration is not mixed with attribute based one.. + /// + internal static string Exception_InvalidRenderingEngineConfiguration { + get { + return ResourceManager.GetString("Exception_InvalidRenderingEngineConfiguration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ModelType parameter cannot be null.. + /// + internal static string Exception_ModelTypeCannotBeNull { + get { + return ResourceManager.GetString("Exception_ModelTypeCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rendering Context cannot be null.. + /// + internal static string Exception_RenderingContextCannotBeNull { + get { + return ResourceManager.GetString("Exception_RenderingContextCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rendering Context Component cannot be null.. + /// + internal static string Exception_RenderingContextComponentCannotBeNull { + get { + return ResourceManager.GetString("Exception_RenderingContextComponentCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A non-nullable value must be provided for the RequestMappings action list.. + /// + internal static string Exception_RequestMappings_Required { + get { + return ResourceManager.GetString("Exception_RequestMappings_Required", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected to find '{0}' method on type '{1}'.. + /// + internal static string Exception_RouteDoesNotHaveAsRouteMethod { + get { + return ResourceManager.GetString("Exception_RouteDoesNotHaveAsRouteMethod", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot find field '{0}' on Component '{1}'.. + /// + internal static string Exception_ScTextComponentFieldNameCannotBeNull { + get { + return ResourceManager.GetString("Exception_ScTextComponentFieldNameCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 'sc-text' {0} property cannot be null.. + /// + internal static string Exception_ScTextComponentNull { + get { + return ResourceManager.GetString("Exception_ScTextComponentNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SitecoreLayout cannot be null.. + /// + internal static string Exception_SitecoreLayoutCannotBeNull { + get { + return ResourceManager.GetString("Exception_SitecoreLayoutCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Sitecore Rendering Engine Middleware cannot be enabled without the Rendering Engine Services being registered first. Did you forget to invoke the AddSitecoreRenderingEngine() extension method in Startup.ConfigureServices?. + /// + internal static string Exception_SitecoreRenderingEngineServicesNotRegistered { + get { + return ResourceManager.GetString("Exception_SitecoreRenderingEngineServicesNotRegistered", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' must implement '{1}'. + /// + internal static string Exception_TypeMustImplementInterface { + get { + return ResourceManager.GetString("Exception_TypeMustImplementInterface", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected model type: {0}.. + /// + internal static string Exception_UnexpectedModelType { + get { + return ResourceManager.GetString("Exception_UnexpectedModelType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ViewContext parameter cannot be null.. + /// + internal static string Exception_ViewContextCannotBeNull { + get { + return ResourceManager.GetString("Exception_ViewContextCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Placeholder name was not defined.. + /// + internal static string Warning_PlaceholderNameWasNotDefined { + get { + return ResourceManager.GetString("Warning_PlaceholderNameWasNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Placeholder '{0}' was empty.. + /// + internal static string Warning_PlaceholderWasEmpty { + get { + return ResourceManager.GetString("Warning_PlaceholderWasEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Placeholder was not defined.. + /// + internal static string Warning_PlaceholderWasNotDefined { + get { + return ResourceManager.GetString("Warning_PlaceholderWasNotDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}: Render method called. Component name: {1}. + /// + internal static string Warning_RenderMethodCalled { + get { + return ResourceManager.GetString("Warning_RenderMethodCalled", resourceCulture); + } + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.resx b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.resx new file mode 100644 index 0000000..802e4d6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Properties/Resources.resx @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to dynamically create an instance of '{0}' + modelType + + + Unable to find appropriate 'BindView' method to execute for the model. + + + Cannot find a valid renderer for the type {0}. + renderType + + + Component with id '{0}' name cannot be null or empty. + componentId + + + The component renderer descriptor for {0} is null. Please ensure that correct Sitecore component-to-view mappings are defined as part of the AddSitecoreRenderingEngine options in Startup.ConfigureServices. + componentName + + + SitecoreRenderingContext controller cannot be null. + + + Could not resolve endpoint data. + + + HttpRequest cannot be null. + + + RenderingEngineMiddleware can only be executed once. Make sure global rendering engine configuration is not mixed with attribute based one. + + + ModelType parameter cannot be null. + + + Rendering Context cannot be null. + + + Rendering Context Component cannot be null. + + + A non-nullable value must be provided for the RequestMappings action list. + + + Expected to find '{0}' method on type '{1}'. + methodName,typeName + + + Cannot find field '{0}' on Component '{1}'. + fieldName,componentName + + + 'sc-text' {0} property cannot be null. + componentTypeName + + + SitecoreLayout cannot be null. + + + The Sitecore Rendering Engine Middleware cannot be enabled without the Rendering Engine Services being registered first. Did you forget to invoke the AddSitecoreRenderingEngine() extension method in Startup.ConfigureServices? + + + '{0}' must implement '{1}' + type,interfaceType + + + Unexpected model type: {0}. + modelType + + + ViewContext parameter cannot be null. + + + Placeholder name was not defined. + + + Placeholder '{0}' was empty. + placeholderName + + + Placeholder was not defined. + + + {0}: Render method called. Component name: {1} + typeName,componentName + + \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs new file mode 100644 index 0000000..cf6e541 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs @@ -0,0 +1,44 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// Service descriptor for a . +/// +/// +/// Initializes a new instance of the class. +/// +/// The predicate to use when retrieving a . +/// The factory method to create a new instance of the . +public class ComponentRendererDescriptor( + Predicate match, + Func factory) +{ + private readonly Func _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + private readonly object _lock = new(); + + private IComponentRenderer? _instance; + + /// + /// Gets a predicate used for matching Sitecore layout components. + /// + public Predicate Match { get; } = match ?? throw new ArgumentNullException(nameof(match)); + + /// + /// Gets an instance of an , creating one if it has not yet been instantiated. + /// + /// The . + /// An instance of an . + public IComponentRenderer GetOrCreate(IServiceProvider services) + { + ArgumentNullException.ThrowIfNull(services); + + if (_instance == null) + { + lock (_lock) + { + _instance = _factory(services); + } + } + + return _instance; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererFactory.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererFactory.cs new file mode 100644 index 0000000..fd99521 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererFactory.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +public class ComponentRendererFactory : IComponentRendererFactory +{ + private readonly IServiceProvider _services; + private readonly RenderingEngineOptions _options; + private readonly ConcurrentDictionary _cache = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The for this instance. + /// The services used for component renderer resolution. + public ComponentRendererFactory( + IOptions options, + IServiceProvider services) + { + ArgumentNullException.ThrowIfNull(options); + _services = services ?? throw new ArgumentNullException(nameof(services)); + _options = options.Value; + } + + /// + public IComponentRenderer GetRenderer(Component component) + { + ArgumentNullException.ThrowIfNull(component); + ArgumentException.ThrowIfNullOrWhiteSpace(component.Name, nameof(component) + nameof(component.Name)); + + return _cache.GetOrAdd(component.Name, _ => + { + ComponentRendererDescriptor? defaultRendererDescriptor = _options.DefaultRenderer; + KeyValuePair matchedRendererDescriptor = + _options.RendererRegistry.FirstOrDefault(x => x.Value.Match(component.Name)); + + ComponentRendererDescriptor rendererDescriptor = + (matchedRendererDescriptor.Value ?? defaultRendererDescriptor) ?? + throw new InvalidOperationException( + string.Format(Resources.Exception_ComponentRendererDescriptorIsNull, component.Name)); + using IServiceScope scope = _services.CreateScope(); + return rendererDescriptor.GetOrCreate(scope.ServiceProvider); + }); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/EditableChromeRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/EditableChromeRenderer.cs new file mode 100644 index 0000000..7ee2632 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/EditableChromeRenderer.cs @@ -0,0 +1,29 @@ +using System.Text; +using Microsoft.AspNetCore.Html; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +public class EditableChromeRenderer : IEditableChromeRenderer +{ + /// + public IHtmlContent Render(EditableChrome chrome) + { + ArgumentNullException.ThrowIfNull(chrome); + ArgumentException.ThrowIfNullOrWhiteSpace(chrome.Name, nameof(chrome) + nameof(chrome.Name)); + + StringBuilder chromeTagString = new($"<{chrome.Name}"); + + foreach (KeyValuePair entry in chrome.Attributes) + { + chromeTagString.Append($" {entry.Key}='{entry.Value}'"); + } + + chromeTagString.Append('>'); + chromeTagString.Append($"{chrome.Content}"); + chromeTagString.Append($""); + + return new HtmlString(chromeTagString.ToString()); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRenderer.cs new file mode 100644 index 0000000..5398104 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRenderer.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// Supports rendering HTML content that can be used in a web page. +/// +public interface IComponentRenderer +{ + /// + /// Generates the output HTML. + /// + /// The current . + /// The current . + /// The HTML content to render. + Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRendererFactory.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRendererFactory.cs new file mode 100644 index 0000000..7f987d0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IComponentRendererFactory.cs @@ -0,0 +1,16 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// Creates the appropriate instance of an for a given . +/// +public interface IComponentRendererFactory +{ + /// + /// Retrieves an that has been configured to render the specified . + /// + /// The that requires rendering. + /// An instance of an that has been configured to render the component. + IComponentRenderer GetRenderer(Component component); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IEditableChromeRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IEditableChromeRenderer.cs new file mode 100644 index 0000000..9097ced --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/IEditableChromeRenderer.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Html; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// Supports rendering chrome data as HTML content that can be used to edit a web page. +/// +public interface IEditableChromeRenderer +{ + /// + /// Generates the output HTML. + /// + /// The chrome data to render. + /// The HTML content to render. + IHtmlContent Render(EditableChrome chrome); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs new file mode 100644 index 0000000..9b48514 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// An that writes to a specified log instead of generating HTML output. +/// +/// +/// Initializes a new instance of the class. +/// +/// The to use for logging. +public class LoggingComponentRenderer(ILogger logger) + : IComponentRenderer +{ + private static readonly IHtmlContent HtmlContent = new HtmlString(string.Empty); + + private readonly ILogger _logger = logger != null ? logger : throw new ArgumentNullException(nameof(logger)); + + /// + /// Creates an instance of a for the class. + /// + /// A predicate to use when attempting to match a layout component. + /// An instance of that describes the . + public static ComponentRendererDescriptor Describe(Predicate match) + { + ArgumentNullException.ThrowIfNull(match); + + return new ComponentRendererDescriptor( + match, + sp => ActivatorUtilities.CreateInstance(sp)); + } + + /// + public Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(renderingContext); + ArgumentNullException.ThrowIfNull(viewContext); + + _logger.LogWarning("{TypeName}: Render method called. Component name: {ComponentName}", GetType().Name, renderingContext.Component?.Name); + + return Task.FromResult(HtmlContent); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ModelBoundViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ModelBoundViewComponentComponentRenderer.cs new file mode 100644 index 0000000..369e807 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ModelBoundViewComponentComponentRenderer.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// An that will render View Components that are bound to a specified Model type. +/// +/// The model type to bind to the View Component. +public class ModelBoundViewComponentComponentRenderer : IComponentRenderer +{ + private readonly string _locator; + private readonly string _viewName; + + /// + /// Initializes a new instance of the class. + /// + /// The string to use when locating the View Component. + /// The name of the view to use when rendering. + public ModelBoundViewComponentComponentRenderer(string locator, string viewName) + { + ArgumentNullException.ThrowIfNull(locator); + ArgumentNullException.ThrowIfNull(viewName); + + _locator = locator; + _viewName = viewName; + } + + /// + public Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(renderingContext); + ArgumentNullException.ThrowIfNull(viewContext); + IViewComponentHelper viewComponentHelper = renderingContext.RenderingHelpers?.ViewComponentHelper ?? throw new NullReferenceException(); + ((IViewContextAware)viewComponentHelper).Contextualize(viewContext); + + return viewComponentHelper.InvokeAsync(_locator, new { modelType = typeof(TModel), viewName = _viewName }); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs new file mode 100644 index 0000000..1fabce1 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// An that will render Partial Views. +/// +public class PartialViewComponentRenderer : IComponentRenderer +{ + private readonly string _locator; + + /// + /// Initializes a new instance of the class. + /// + /// The string to use when locating the Partial View. + public PartialViewComponentRenderer(string locator) + { + ArgumentException.ThrowIfNullOrWhiteSpace(locator); + _locator = locator; + } + + /// + /// Creates an instance of a for the class. + /// + /// A predicate to use when attempting to match a layout component. + /// The string to use when locating the Partial View. + /// An instance of that describes the . + public static ComponentRendererDescriptor Describe(Predicate match, string locator) + { + ArgumentNullException.ThrowIfNull(match); + ArgumentException.ThrowIfNullOrWhiteSpace(locator); + return new ComponentRendererDescriptor( + match, + sp => ActivatorUtilities.CreateInstance(sp, locator)); + } + + /// + public Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(renderingContext); + ArgumentNullException.ThrowIfNull(viewContext); + IHtmlHelper htmlHelper = renderingContext.RenderingHelpers?.HtmlHelper ?? throw new NullReferenceException(); + ((IViewContextAware)htmlHelper).Contextualize(viewContext); + + return htmlHelper.PartialAsync(_locator, renderingContext.Component); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/RenderingHelpers.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/RenderingHelpers.cs new file mode 100644 index 0000000..ccb1180 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/RenderingHelpers.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// Rendering helpers. +/// +/// +/// Initializes a new instance of the class. +/// +/// The IViewComponentHelper instance . +/// The IHtmlHelper instance . +public class RenderingHelpers(IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) +{ + /// + /// Gets IViewComponentHelper instance. + /// + public IViewComponentHelper ViewComponentHelper { get; private set; } = viewComponentHelper; + + /// + /// Gets IHtmlHelper instance. + /// + public IHtmlHelper HtmlHelper { get; private set; } = htmlHelper; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/SitecoreRenderingContext.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/SitecoreRenderingContext.cs new file mode 100644 index 0000000..0b4b141 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/SitecoreRenderingContext.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +public class SitecoreRenderingContext : ISitecoreRenderingContext +{ + /// + public SitecoreLayoutResponse? Response { get; set; } + + /// + public Component? Component { get; set; } + + /// + public ControllerBase? Controller { get; set; } + + /// + public RenderingHelpers? RenderingHelpers { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs new file mode 100644 index 0000000..73dc497 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +/// +/// An that will render View Components. +/// +public class ViewComponentComponentRenderer : IComponentRenderer +{ + private readonly string _locator; + + /// + /// Initializes a new instance of the class. + /// + /// The string to use when locating the View Component. + public ViewComponentComponentRenderer(string locator) + { + ArgumentException.ThrowIfNullOrWhiteSpace(locator); + _locator = locator; + } + + /// + /// Creates an instance of a for the class. + /// + /// A predicate to use when attempting to match a layout component. + /// The string to use when locating the View Component. + /// An instance of . + public static ComponentRendererDescriptor Describe(Predicate match, string locator) + { + ArgumentNullException.ThrowIfNull(match); + ArgumentException.ThrowIfNullOrWhiteSpace(locator); + + return new ComponentRendererDescriptor( + match, + sp => ActivatorUtilities.CreateInstance(sp, locator)); + } + + /// + public Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext) + { + ArgumentNullException.ThrowIfNull(renderingContext); + ArgumentNullException.ThrowIfNull(viewContext); + + IViewComponentHelper viewComponentHelper = renderingContext.RenderingHelpers?.ViewComponentHelper ?? throw new NullReferenceException(); + ((IViewContextAware)viewComponentHelper).Contextualize(viewContext); + + return viewComponentHelper.InvokeAsync(_locator); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/RenderingEngineConstants.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/RenderingEngineConstants.cs new file mode 100644 index 0000000..25e4a98 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/RenderingEngineConstants.cs @@ -0,0 +1,124 @@ +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine; + +/// +/// Various constants relevant to the Sitecore Rendering Engine. +/// +public static class RenderingEngineConstants +{ + /// + /// The name of the default partial view used when a requested component cannot be found. + /// + public const string DefaultMissingComponentPartialViewName = "_ComponentNotFound"; + + /// + /// Constants relevant to the Sitecore tag helpers. + /// + public static class SitecoreTagHelpers + { + /// + /// The HTML tag used by the tag helper. + /// + public const string RichTextHtmlTag = "sc-text"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string LinkHtmlTag = "sc-link"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string DateHtmlTag = "sc-date"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string NumberHtmlTag = "sc-number"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string ImageHtmlTag = "sc-img"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string FileHtmlTag = "sc-file"; + + /// + /// The name of the asp-for attribute. + /// + public const string AspForTagHelperAttribute = "asp-for"; + + /// + /// The HTML tag used by the tag helper. + /// + public const string PlaceholderHtmlTag = "sc-placeholder"; + + /// + /// The name of the rich-text attribute. + /// + public const string TextTagHelperAttribute = "asp-text"; + + /// + /// The name of the image attribute. + /// + public const string ImageTagHelperAttribute = "asp-image"; + + /// + /// The name of the date attribute. + /// + public const string DateTagHelperAttribute = "asp-date"; + + /// + /// The name of the number attribute. + /// + public const string NumberTagHelperAttribute = "asp-number"; + + /// + /// The name of the link attribute. + /// + public const string LinkTagHelperAttribute = "asp-link"; + + /// + /// The name of the file attribute. + /// + public const string FileTagHelperAttribute = "asp-file"; + } + + /// + /// Constants relevant to Sitecore view components. + /// + public static class SitecoreViewComponents + { + /// + /// The name of the default Sitecore View Component. + /// + public const string DefaultSitecoreViewComponentName = "SitecoreComponent"; + } + + /// + /// Constants relevant to Sitecore localization functionality. + /// + public static class SitecoreLocalization + { + /// + /// The name of request culture prefix in the route. + /// + public const string RequestCulturePrefix = "culture"; + } + + /// + /// Constants relevant to Request Route Values. + /// + public static class RouteValues + { + /// + /// The name of sitecoreRoute the request. + /// + public const string SitecoreRoute = "sitecoreRoute"; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Routing/LanguageRouteConstraint.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Routing/LanguageRouteConstraint.cs new file mode 100644 index 0000000..1fab73e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Routing/LanguageRouteConstraint.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Routing; + +/// +public class LanguageRouteConstraint : IRouteConstraint +{ + /// + public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + ArgumentException.ThrowIfNullOrWhiteSpace(routeKey); + ArgumentNullException.ThrowIfNull(values); + ArgumentNullException.ThrowIfNull(routeDirection); + + if (!values.TryGetValue("culture", out object? value)) + { + return false; + } + + string? lang = value?.ToString(); + CultureInfo? culture = null; + if (!string.IsNullOrEmpty(lang)) + { + culture = CultureInfo.GetCultures(CultureTypes.AllCultures) + .SingleOrDefault(c => c.IetfLanguageTag.Equals(lang, StringComparison.InvariantCultureIgnoreCase)); + } + + return culture != null; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs new file mode 100644 index 0000000..9f23b1e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs @@ -0,0 +1,59 @@ +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +/// +/// GraphQl Site Collection Service. +/// +/// The GraphQl Client. +/// The Logger. +internal class GraphQlSiteCollectionService(IGraphQLClient graphQlClient, ILogger logger) + : ISiteCollectionService +{ + private const string SiteInfoCollectionQuery = """ + query SiteInfoCollectionQuery { + site { + siteInfoCollection { + name + hostname + } + } + } + """; + + /// + /// Get the Sites Collection. + /// + /// A returning an array of or null of none were returned. + public async Task GetSitesCollection() + { + GraphQLRequest siteInfoCollectionRequest = new() + { + Query = SiteInfoCollectionQuery, + OperationName = "SiteInfoCollectionQuery" + }; + + try + { + GraphQLResponse response = await graphQlClient.SendQueryAsync(siteInfoCollectionRequest).ConfigureAwait(false); + + if (response.Errors != null && response.Errors.Length != 0) + { + foreach (GraphQLError graphQlError in response.Errors) + { + logger.LogError("[GraphQL Client Error] {Message}", graphQlError.Message); + } + } + + return response.Data.Site?.SiteInfoCollection; + } + catch (Exception exception) + { + logger.LogError("[GraphQL Client Error] {exceptionMessage}", exception.Message); + return null; + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteCollectionService.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteCollectionService.cs new file mode 100644 index 0000000..315334d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteCollectionService.cs @@ -0,0 +1,15 @@ +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +/// +/// Service Interface to retrieve the Site Collection. +/// +public interface ISiteCollectionService +{ + /// + /// Get Sites Collection. + /// + /// A returning a array or null on completion. + Task GetSitesCollection(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteResolver.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteResolver.cs new file mode 100644 index 0000000..25fae60 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/ISiteResolver.cs @@ -0,0 +1,14 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +/// +/// Provides methods to resolve Sites. +/// +public interface ISiteResolver +{ + /// + /// Gets a Site name by host name. + /// + /// The host name. + /// A returning a site name or null when no site was found. + Task GetByHost(string hostName); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/SiteResolver.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/SiteResolver.cs new file mode 100644 index 0000000..af6f825 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/SiteResolver.cs @@ -0,0 +1,81 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Services; + +/// +internal class SiteResolver(ISiteCollectionService siteCollectionService) + : ISiteResolver +{ + private readonly ISiteCollectionService _siteCollectionService = siteCollectionService ?? throw new ArgumentNullException(nameof(siteCollectionService)); + + /// + public async Task GetByHost(string hostName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(hostName); + + Dictionary hostMap = await GetHostMapAsync().ConfigureAwait(false); + foreach (KeyValuePair pair in hostMap) + { + if (MatchesPattern(hostName, pair.Key)) + { + return pair.Value.Name; + } + } + + return null; + } + + private static bool MatchesPattern(string hostname, string pattern) + { + // Dots should be treated as chars + // Stars should be treated as wildcards + string regExpPattern = pattern.Replace(".", "\\.", StringComparison.Ordinal).Replace("*", ".*", StringComparison.Ordinal); + + Regex regExp = new($"^{regExpPattern}$", RegexOptions.IgnoreCase); + + return regExp.IsMatch(hostname); + } + + private async Task> GetHostMapAsync() + { + Dictionary map = []; + + SiteInfo?[]? siteCollection = await _siteCollectionService.GetSitesCollection().ConfigureAwait(false); + + if (siteCollection != null) + { + foreach (SiteInfo site in siteCollection.OfType()) + { + string[]? hostNames = site.HostName? + .Replace(" ", string.Empty, StringComparison.Ordinal) + .ToLower(CultureInfo.InvariantCulture).Split('|'); + + if (hostNames != null) + { + foreach (string hostName in hostNames) + { + map.TryAdd(hostName, site); + } + } + } + } + + // Now order by specificity. + // This is equivalent to sorting from longest to shortest, assuming + // that a match is less specific as wildcards are introduced. + // E.g., order.eu.site.com → *.eu.site.com → *.site.com → * + // In case of a tie (e.g., *.site.com vs i.site.com), prefer the one with fewer wildcards. + return new Dictionary( + map.OrderBy(pair => + { + if (pair.Key.Length == 0) + { + return 0; + } + + return pair.Key.Count(c => c == '*') - pair.Key.Length; + })); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Sitecore.AspNetCore.SDK.RenderingEngine.csproj b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Sitecore.AspNetCore.SDK.RenderingEngine.csproj new file mode 100644 index 0000000..e84fa6d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Sitecore.AspNetCore.SDK.RenderingEngine.csproj @@ -0,0 +1,54 @@ + + + + Sitecore Rendering Host + .NET SDK for creating a Sitecore Headless Rendering Host + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.RenderingEngine.Tests + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests + + + + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/ComponentHolder.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/ComponentHolder.cs new file mode 100644 index 0000000..e981a36 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/ComponentHolder.cs @@ -0,0 +1,35 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers; + +/// +/// Represents a usage context for a Component. +/// +internal class ComponentHolder : IDisposable +{ + private readonly ISitecoreRenderingContext _renderingContext; + + private readonly Component? _oldContextComponent; + + /// + /// Initializes a new instance of the class. + /// Starts a new component rendering context. + /// + /// The Rendering Context. + /// The Component. + public ComponentHolder(ISitecoreRenderingContext renderingContext, Component component) + { + _renderingContext = renderingContext ?? throw new ArgumentNullException(nameof(renderingContext)); + _oldContextComponent = _renderingContext.Component; + _renderingContext.Component = component ?? throw new ArgumentNullException(nameof(component)); + } + + /// + /// Ends the component rendering context and reinstates the prior context. + /// + public void Dispose() + { + _renderingContext.Component = _oldContextComponent; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs new file mode 100644 index 0000000..c52a0c9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs @@ -0,0 +1,73 @@ +using System.Globalization; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders HTML date for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute)] +public class DateTagHelper : TagHelper +{ + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + /// Gets or sets a format for the date. + /// + public string? DateFormat { get; set; } + + /// + /// Gets or sets a culture for the number. + /// + public string? Culture { get; set; } + + /// + /// Gets or sets the date value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute)] + public DateField? DateModel { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (output.TagName != null && output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag, StringComparison.OrdinalIgnoreCase)) + { + output.TagName = null; + } + + DateField? field = DateModel ?? For?.Model as DateField; + + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field?.EditableMarkup); + + if (field == null || (field.Value.Equals(DateTime.MinValue) && !outputEditableMarkup)) + { + return; + } + + CultureInfo culture = !string.IsNullOrWhiteSpace(Culture) ? CultureInfo.CreateSpecificCulture(Culture) : CultureInfo.CurrentCulture; + + string formattedDate = !string.IsNullOrWhiteSpace(DateFormat) ? field.Value.ToString(DateFormat, culture) : field.Value.ToString(culture); + + HtmlString html = outputEditableMarkup ? new HtmlString(field.EditableMarkup) : new HtmlString(formattedDate); + + output.Content.SetHtmlContent(html); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/FileTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/FileTagHelper.cs new file mode 100644 index 0000000..ed4f03a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/FileTagHelper.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using File = Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties.File; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders HTML file download link for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.FileTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.FileTagHelperAttribute)] +public class FileTagHelper : TagHelper +{ + private const string FileLinkTag = "a"; + private const string HrefAttribute = "href"; + private const string TypeAttribute = "type"; + private const string TitleAttribute = "title"; + private const string TargetAttribute = "target"; + + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating target attribute. + /// + public string Target { get; set; } = string.Empty; + + /// + /// Gets or sets the file value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.FileTagHelperAttribute)] + public FileField? FileModel { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (output.TagName != null && output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag, StringComparison.OrdinalIgnoreCase)) + { + output.TagName = FileLinkTag; + output.TagMode = TagMode.StartTagAndEndTag; + } + + FileField? field = FileModel ?? For?.Model as FileField; + + File? file = field?.Value; + if (file == null || string.IsNullOrWhiteSpace(file.Src)) + { + return; + } + + if (!output.Attributes.ContainsName(HrefAttribute)) + { + output.Attributes.Add(HrefAttribute, file.Src); + } + + if (!string.IsNullOrWhiteSpace(file.MimeType) && !output.Attributes.ContainsName(TypeAttribute)) + { + output.Attributes.Add(TypeAttribute, file.MimeType); + } + + if (!string.IsNullOrWhiteSpace(file.Description) && !output.Attributes.ContainsName(TitleAttribute)) + { + output.Attributes.Add(TitleAttribute, file.Description); + } + + if (!string.IsNullOrWhiteSpace(Target)) + { + output.Attributes.Add(TargetAttribute, Target); + } + + string? innerContent = output.GetChildContentAsync()?.Result?.GetContent(); + if (!string.IsNullOrWhiteSpace(innerContent)) + { + output.Content.AppendHtml(innerContent); + } + else if (!string.IsNullOrWhiteSpace(file.Title)) + { + output.Content.Append(file.Title); + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs new file mode 100644 index 0000000..3044a67 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs @@ -0,0 +1,225 @@ +using HtmlAgilityPack; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders HTML hyperlink for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.ImageTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("img", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("img", Attributes = RenderingEngineConstants.SitecoreTagHelpers.ImageTagHelperAttribute)] +public class ImageTagHelper : TagHelper +{ + private const string ImgTag = "img"; + private const string ScrAttribute = "src"; + private const string AltAttribute = "alt"; + private const string ClassAttribute = "class"; + private const string WidthAttribute = "width"; + private const string HeightAttribute = "height"; + private const string HSpaceAttribute = "hspace"; + private const string VSpaceAttribute = "vspace"; + private const string TitleAttribute = "title"; + private const string BorderAttribute = "border"; + + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + /// Gets or sets the image value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.ImageTagHelperAttribute)] + public ImageField? ImageModel { get; set; } + + /// + /// Gets or sets parameters that are passed to Sitecore to perform server-side resizing of the image. + /// + public object? ImageParams { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (output.TagName != null && output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag, StringComparison.OrdinalIgnoreCase)) + { + output.TagName = null; + } + + ImageField? field = ImageModel ?? For?.Model as ImageField; + + if (field == null || string.IsNullOrWhiteSpace(field.Value.Src)) + { + return; + } + + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); + + if (outputEditableMarkup) + { + output.TagName = null; + output.Content.SetHtmlContent(MergeEditableMarkupWithCustomAttributes(field.EditableMarkup, field, output)); + } + else + { + if (output.TagName == null) + { + output.Content.SetHtmlContent(GenerateImage(field, output)); + } + else + { + output.Attributes.Add(ScrAttribute, field.GetMediaLink(ImageParams)); + + if (!string.IsNullOrWhiteSpace(field.Value.Alt)) + { + output.Attributes.Add(AltAttribute, field.Value.Alt); + } + + if (!string.IsNullOrWhiteSpace(field.Value.Class)) + { + output.Attributes.Add(ClassAttribute, field.Value.Class); + } + + if (field.Value.Border.HasValue) + { + output.Attributes.Add(BorderAttribute, field.Value.Border.ToString()); + } + + if (!string.IsNullOrWhiteSpace(field.Value.Title)) + { + output.Attributes.Add(TitleAttribute, field.Value.Title); + } + + if (field.Value.HSpace.HasValue) + { + output.Attributes.Add(HSpaceAttribute, field.Value.HSpace.ToString()); + } + + if (field.Value.VSpace.HasValue) + { + output.Attributes.Add(VSpaceAttribute, field.Value.VSpace.ToString()); + } + + if (field.Value.Width.HasValue) + { + output.Attributes.Add(WidthAttribute, field.Value.Width.ToString()); + } + + if (field.Value.Height.HasValue) + { + output.Attributes.Add(HeightAttribute, field.Value.Height.ToString()); + } + } + } + } + + private TagBuilder GenerateImage(ImageField imageField, TagHelperOutput output) + { + Image image = imageField.Value; + TagBuilder tagBuilder = new(ImgTag) + { + TagRenderMode = TagRenderMode.SelfClosing + }; + + if (!string.IsNullOrWhiteSpace(image.Src)) + { + tagBuilder.Attributes.Add(ScrAttribute, imageField.GetMediaLink(ImageParams)); + } + + if (!string.IsNullOrWhiteSpace(image.Alt)) + { + tagBuilder.MergeAttribute(AltAttribute, image.Alt); + } + + if (!string.IsNullOrWhiteSpace(image.Class)) + { + tagBuilder.MergeAttribute(ClassAttribute, image.Class); + } + + if (image.Border.HasValue) + { + tagBuilder.MergeAttribute(BorderAttribute, image.Border.ToString()); + } + + if (!string.IsNullOrWhiteSpace(image.Title)) + { + tagBuilder.MergeAttribute(TitleAttribute, image.Title); + } + + if (image.HSpace.HasValue) + { + tagBuilder.MergeAttribute(HSpaceAttribute, image.HSpace.ToString()); + } + + if (image.VSpace.HasValue) + { + tagBuilder.MergeAttribute(VSpaceAttribute, image.VSpace.ToString()); + } + + if (image.Width.HasValue) + { + tagBuilder.MergeAttribute(WidthAttribute, image.Width.ToString()); + } + + if (image.Height.HasValue) + { + tagBuilder.MergeAttribute(HeightAttribute, image.Height.ToString()); + } + + foreach (TagHelperAttribute? attribute in output.Attributes) + { + tagBuilder.MergeAttribute(attribute.Name, attribute.Value.ToString(), true); + } + + return tagBuilder; + } + + private HtmlString MergeEditableMarkupWithCustomAttributes(string editableMarkUp, ImageField imageField, TagHelperOutput output) + { + // TODO Can we find a cheaper method to achieve this? Remove the HtmlAgilityPack dependency? + HtmlDocument doc = new(); + doc.LoadHtml(editableMarkUp); + doc.OptionOutputOriginalCase = true; + doc.OptionWriteEmptyNodes = true; + + HtmlNode? imageNode = doc.DocumentNode.SelectSingleNode("img"); + + if (imageNode != null) + { + foreach (TagHelperAttribute? attribute in output.Attributes) + { + if (attribute.Value != null) + { + if (imageNode.Attributes[attribute.Name] == null) + { + imageNode.Attributes.Add(attribute.Name, attribute.Value?.ToString()); + } + else + { + imageNode.SetAttributeValue(attribute.Name, attribute.Value?.ToString()); + } + } + } + + imageNode.SetAttributeValue(ScrAttribute, imageField.GetMediaLink(ImageParams)); + } + + return new HtmlString(doc.DocumentNode.OuterHtml); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs new file mode 100644 index 0000000..350f552 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs @@ -0,0 +1,203 @@ +using System.Text; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders HTML hyperlink for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.LinkTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.LinkTagHelperAttribute)] +public class LinkTagHelper : TagHelper +{ + private const string HrefAttribute = "href"; + private const string TargetAttribute = "target"; + private const string TitleAttribute = "title"; + private const string ClassAttribute = "class"; + private const string AnchorTag = "a"; + private const string RelAttribute = "rel"; + private const string BlankValue = "_blank"; + private const string AnchorValue = "#"; + + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + /// Gets or sets the link value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.LinkTagHelperAttribute)] + public HyperLinkField? LinkModel { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + HyperLinkField? field = LinkModel ?? For?.Model as HyperLinkField; + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field.EditableMarkupLast); + if (field == null || (string.IsNullOrWhiteSpace(field.Value.Href) && !outputEditableMarkup)) + { + return; + } + + if (output.TagName != null && (output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, StringComparison.OrdinalIgnoreCase) || outputEditableMarkup)) + { + output.TagName = null; + } + + if (outputEditableMarkup) + { + RenderEditableMarkup(output, field); + } + else + { + RenderMarkup(output, field); + } + } + + private static void RenderMarkup(TagHelperOutput output, HyperLinkField field) + { + if (output.TagName == null) + { + // generate full anchor markup + output.Content.SetHtmlContent(GenerateLink(field.Value, output)); + } + else + { + HyperLink hyperLink = field.Value; + + output.Attributes.Add(HrefAttribute, BuildHref(hyperLink)); + + if (!string.IsNullOrWhiteSpace(hyperLink.Target) && !output.Attributes.ContainsName(TargetAttribute)) + { + output.Attributes.Add(TargetAttribute, hyperLink.Target); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Title) && !output.Attributes.ContainsName(TitleAttribute)) + { + output.Attributes.Add(TitleAttribute, hyperLink.Title); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Class) && !output.Attributes.ContainsName(ClassAttribute)) + { + output.Attributes.Add(ClassAttribute, hyperLink.Class); + } + + if (hyperLink.Target == BlankValue && !output.Attributes.ContainsName(RelAttribute)) + { + // information disclosure attack prevention keeps target blank site from getting ref to window.opener + output.Attributes.Add(RelAttribute, "noopener noreferrer"); + } + + string? innerContent = output.GetChildContentAsync()?.Result?.GetContent(); + if (string.IsNullOrWhiteSpace(innerContent) && !string.IsNullOrWhiteSpace(hyperLink.Text)) + { + output.Content.Append(field.Value.Text); + } + } + } + + private static void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) + { + DefaultTagHelperContent content = new(); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupFirst)); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupLast)); + output.Content.SetHtmlContent(content); + } + + /// + /// Generates anchor HTML tag. + /// + /// The field. + /// Tag helper output. + /// Anchor HTML tag. + private static TagBuilder GenerateLink(HyperLink hyperLink, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(hyperLink); + + TagBuilder tagBuilder = new(AnchorTag); + + SetTagContent(hyperLink, output, tagBuilder); + + SetAttributes(hyperLink, output, tagBuilder); + + return tagBuilder; + } + + private static void SetAttributes(HyperLink hyperLink, TagHelperOutput output, TagBuilder tagBuilder) + { + tagBuilder.Attributes.Add(HrefAttribute, BuildHref(hyperLink)); + + if (!string.IsNullOrWhiteSpace(hyperLink.Target)) + { + tagBuilder.MergeAttribute(TargetAttribute, hyperLink.Target); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Title)) + { + tagBuilder.MergeAttribute(TitleAttribute, hyperLink.Title); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Class)) + { + tagBuilder.MergeAttribute(ClassAttribute, hyperLink.Class); + } + + foreach (TagHelperAttribute? attribute in output.Attributes) + { + tagBuilder.MergeAttribute(attribute.Name, attribute.Value.ToString(), true); + } + + if (hyperLink.Target == BlankValue && !tagBuilder.Attributes.ContainsKey(RelAttribute)) + { + // information disclosure attack prevention keeps target blank site from getting ref to window.opener + tagBuilder.MergeAttribute(RelAttribute, "noopener noreferrer"); + } + } + + private static void SetTagContent(HyperLink hyperLink, TagHelperOutput output, TagBuilder tagBuilder) + { + TagHelperContent? childContext = output.GetChildContentAsync().Result; + string? existingTagContent = childContext.GetContent(); + + // user tag content has priority + if (!string.IsNullOrWhiteSpace(existingTagContent)) + { + tagBuilder.InnerHtml.SetHtmlContent(existingTagContent); + } + else + { + tagBuilder.InnerHtml.SetContent(hyperLink.Text!); + } + } + + private static string BuildHref(HyperLink hyperLink) + { + StringBuilder sb = new(); + sb.Append(hyperLink.Href); + + if (!string.IsNullOrWhiteSpace(hyperLink.Anchor)) + { + sb.Append(AnchorValue[0]); + sb.Append(hyperLink.Anchor); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs new file mode 100644 index 0000000..6ac8f57 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders text for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute)] +public class NumberTagHelper : TagHelper +{ + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a format for the number. + /// + public string? NumberFormat { get; set; } + + /// + /// Gets or sets a culture for the number. + /// + public string? Culture { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + /// Gets or sets the number value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute)] + public NumberField? NumberModel { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (output.TagName != null && output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag, StringComparison.OrdinalIgnoreCase)) + { + output.TagName = null; + } + + NumberField? field = NumberModel ?? For?.Model as NumberField; + if (field == null) + { + return; + } + + CultureInfo culture = !string.IsNullOrWhiteSpace(Culture) ? CultureInfo.CreateSpecificCulture(Culture) : CultureInfo.CurrentCulture; + + string formattedNumber = string.Empty; + + if (field.Value.HasValue) + { + formattedNumber = !string.IsNullOrWhiteSpace(NumberFormat) ? field.Value.Value.ToString(NumberFormat, culture) : field.Value.Value.ToString(culture); + } + + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); + string value = outputEditableMarkup ? field.EditableMarkup : formattedNumber; + + output.Content.SetHtmlContent(value); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs new file mode 100644 index 0000000..6a2b9e8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders HTML text for a Sitecore . +/// +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute)] +public class RichTextTagHelper : TagHelper +{ + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + /// Gets or sets the rich text value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute)] + public EditableField? TextModel { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (output.TagName != null && output.TagName.Equals(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag, StringComparison.OrdinalIgnoreCase)) + { + output.TagName = null; + } + + if ((TextModel ?? For?.Model) is not RichTextField richTextField) + { + return; + } + + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(richTextField.EditableMarkup); + HtmlString html = outputEditableMarkup + ? new HtmlString(richTextField.EditableMarkup) + : new HtmlString(richTextField.Value); + + output.Content.SetHtmlContent(html); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs new file mode 100644 index 0000000..9291be2 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs @@ -0,0 +1,58 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; + +/// +/// Tag helper that renders text for a Sitecore . +/// +[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] +public partial class TextFieldTagHelper : TagHelper +{ + /// + /// Gets or sets the model value. + /// + [HtmlAttributeName(RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] + public ModelExpression? For { get; set; } + + /// + /// Gets or sets a value indicating whether to convert line endings to
tags. + ///
+ [HtmlAttributeName("convert-new-lines")] + public bool ConvertNewLines { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the field can be edited. + /// + public bool Editable { get; set; } = true; + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + if (For?.Model is not TextField field) + { + return; + } + + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); + string value = outputEditableMarkup ? field.EditableMarkup : field.Value; + + if (outputEditableMarkup || (ConvertNewLines && NewLineRegex().IsMatch(value))) + { + value = NewLineRegex().Replace(value, "
"); + output.Content.SetHtmlContent(value); + } + else + { + output.Content.SetContent(value); + } + } + + [GeneratedRegex("\r?\n")] + private static partial Regex NewLineRegex(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs new file mode 100644 index 0000000..c42ef29 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs @@ -0,0 +1,136 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers; + +/// +/// Tag helper for the Sitecore placeholder element. +/// +/// +/// Initializes a new instance of the class. +/// +/// An instance of . +/// An instance of . +[HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.PlaceholderHtmlTag)] +public class PlaceholderTagHelper( + IComponentRendererFactory componentFactory, + IEditableChromeRenderer chromeRenderer) + : TagHelper +{ + private readonly IComponentRendererFactory _componentRendererFactory = componentFactory ?? throw new ArgumentNullException(nameof(componentFactory)); + + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + + /// + /// Gets or sets the current view context for the tag helper. + /// + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext? ViewContext { get; set; } + + /// + /// Gets or sets the name of the placeholder to be rendered. + /// + public string? Name { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + string? placeholderName = Name; + + if (string.IsNullOrEmpty(placeholderName)) + { + output.Content.SetHtmlContent($""); + return; + } + + output.TagName = string.Empty; + if (ViewContext == null) + { + throw new NullReferenceException(Resources.Exception_ViewContextCannotBeNull); + } + + ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext() ?? + throw new NullReferenceException(Resources.Exception_SitecoreLayoutCannotBeNull); + Placeholder? placeholderFeatures = GetPlaceholderFeatures(placeholderName, renderingContext); + + if (placeholderFeatures == null) + { + output.Content.SetHtmlContent($""); + return; + } + + bool foundPlaceholderFeatures = false; + + foreach (IPlaceholderFeature placeholderFeature in placeholderFeatures.OfType()) + { + foundPlaceholderFeatures = true; + + IHtmlContent html; + switch (placeholderFeature) + { + case Component component: + using (new ComponentHolder(renderingContext, component)) + { + html = await RenderComponent(renderingContext, component) + .ConfigureAwait(false); + } + + break; + case EditableChrome chrome: + html = _chromeRenderer.Render(chrome); + break; + default: + html = HtmlString.Empty; + break; + } + + output.Content.AppendHtml(html); + } + + if (!foundPlaceholderFeatures) + { + output.Content.SetHtmlContent($""); + } + } + + private static Placeholder? GetPlaceholderFeatures(string placeholderName, ISitecoreRenderingContext renderingContext) + { + Placeholder? placeholderFeatures = null; + + // try to get the placeholder from the "context" component + renderingContext.Component?.Placeholders.TryGetValue(placeholderName, out placeholderFeatures); + + // top level placeholders do not have a "context" component set, so their component list can be retrieved directly from the Sitecore Route object + if (placeholderFeatures?.Count > 0) + { + return placeholderFeatures; + } + + Route? route = renderingContext.Response?.Content?.Sitecore?.Route; + route?.Placeholders.TryGetValue(placeholderName, out placeholderFeatures); + + return placeholderFeatures; + } + + private Task RenderComponent(ISitecoreRenderingContext renderingContext, Component component) + { + if (ViewContext == null) + { + throw new NullReferenceException(Resources.Exception_RenderingContextCannotBeNull); + } + + IComponentRenderer renderer = _componentRendererFactory.GetRenderer(component); + return renderer.Render(renderingContext, ViewContext); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/BindingViewComponent.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/BindingViewComponent.cs new file mode 100644 index 0000000..ec2607a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/BindingViewComponent.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.ViewComponents; + +/// +/// Provides delayed binding for a . +/// +public abstract class BindingViewComponent : ViewComponent +{ + /// + /// Initializes a new instance of the class. + /// + /// The to enable binding. + protected BindingViewComponent(IViewModelBinder binder) + { + ArgumentNullException.ThrowIfNull(binder); + Binder = binder; + } + + /// + /// Gets the instance of used for binding. + /// + protected IViewModelBinder Binder { get; } + + /// + /// Returns the default view using a bound model of . + /// + /// The model to be bound. + /// The . + public virtual async Task BindView() + where TModel : class, new() + { + TModel model = await Binder.Bind(ViewContext).ConfigureAwait(false); + return View(model); + } + + /// + /// Returns the default view bound to the given model. + /// + /// The model to be bound. + /// An instance of to be bound. + /// The . + public virtual async Task BindView(TModel model) + where TModel : class + { + await Binder.Bind(model, ViewContext).ConfigureAwait(false); + return View(model); + } + + /// + /// Returns the specified view using a bound model of . + /// + /// The model to be bound. + /// The view to be returned. + /// The . + public virtual async Task BindView(string viewName) + where TModel : class, new() + { + TModel model = await Binder.Bind(ViewContext).ConfigureAwait(false); + return View(viewName, model); + } + + /// + /// Returns the specified view bound to the given model. + /// + /// The model to be bound. + /// The view to be returned. + /// An instance of to be bound. + /// The . + public virtual async Task BindView(string viewName, TModel model) + where TModel : class + { + await Binder.Bind(model, ViewContext).ConfigureAwait(false); + return View(viewName, model); + } + + /// + /// Returns the specified view bound to the given model type. + /// + /// The model type to be used for binding. + /// The view to be returned. + /// The . + public virtual async Task BindView(Type modelType, string viewName) + { + object model = await Binder.Bind(modelType, ViewContext).ConfigureAwait(false); + return View(viewName, model); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/SitecoreComponentViewComponent.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/SitecoreComponentViewComponent.cs new file mode 100644 index 0000000..8873887 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/ViewComponents/SitecoreComponentViewComponent.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.ViewComponents; + +/// +/// Default view with model binding to render components. +/// +/// +/// Initializes a new instance of the class. +/// +/// The to use when binding the model to the view component. +[ViewComponent] +public class SitecoreComponentViewComponent(IViewModelBinder binder) + : BindingViewComponent(binder) +{ + /// + /// Executes the view component. + /// + /// The type of the model to use when generating the view component output. + /// The name of the view to use when generating the view component output. + /// A representing the result of the asynchronous operation. + public async Task InvokeAsync(Type modelType, string viewName) + { + ArgumentNullException.ThrowIfNull(modelType); + ArgumentException.ThrowIfNullOrWhiteSpace(viewName); + return await BindView(modelType, viewName).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs new file mode 100644 index 0000000..7498a28 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Middleware; +using Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Extensions; + +/// +/// Sitecore redirects configuration. +/// +public static class SitecoreRedirectsAppConfigurationExtensions +{ + /// + /// Configures Sitecore redirects. In case of global configuration of RenderingEngine. + /// + /// Application builder. + /// Static rewrite options that will be merged with Sitecore rewrite options. + /// Modified application builder. + public static IApplicationBuilder UseSitecoreRedirects(this IApplicationBuilder app, RewriteOptions? staticRewriteOptions = null) + { + ArgumentNullException.ThrowIfNull(app); + + if (staticRewriteOptions is null) + { + return app.UseMiddleware(); + } + + return app.UseMiddleware(Options.Create(staticRewriteOptions)); + } + + /// + /// Configuration of Sitecore redirects functionality. + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddSitecoreRedirects(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.TryAddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs new file mode 100644 index 0000000..bb3f221 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using ProxyKit; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; +using Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Extensions; + +/// +/// Sitemap configuration. +/// +public static partial class SitemapAppConfigurationExtensions +{ + /// + /// Configures sitemap. In case of global configuration of RenderingEngine. + /// + /// Application builder. + /// Modified application builder. + public static IApplicationBuilder UseSitemap(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + ISitemapService? sitemapService = app.ApplicationServices.GetService(); + + if (sitemapService == null) + { + return app; + } + + app.MapWhen( + context => + { + if (context.Request.Path.HasValue) + { + return SitemapRegex().Match(context.Request.Path.Value).Success; + } + + return false; + }, + api => + { + api.RunProxy(async context => + { + if (context.Request.Path.HasValue) + { + context.TryGetResolvedSiteName(out string? resolvedSiteName); + + string url = await sitemapService.GetSitemapUrl(context.Request.Path.Value, resolvedSiteName).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(url)) + { + context.Request.Path = string.Empty; + ForwardContext? forwardContext = context.ForwardTo(url); + return await forwardContext.Send().ConfigureAwait(false); + } + } + + return new HttpResponseMessage(HttpStatusCode.NotFound); + }); + }); + + return app; + } + + /// + /// Configuration of Edge sitemap functionality, which is using GraphQL. + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddEdgeSitemap(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddProxy(); + + services.TryAddSingleton(); + + return services; + } + + /// + /// Configuration of Sitemap functionality, which is proxies to specified instance Uri. + /// + /// The to add services to. + /// The configuration needed to resolve sitemap. + /// The so that additional calls can be chained. + public static IServiceCollection AddSitemap(this IServiceCollection services, Action configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.AddProxy(); + + services.Configure(configuration); + + services.AddSingleton(); + + return services; + } + + [GeneratedRegex("^/sitemap(-[0-9]{1,2})?.xml$")] + private static partial Regex SitemapRegex(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectInfo.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectInfo.cs new file mode 100644 index 0000000..8829ea6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectInfo.cs @@ -0,0 +1,27 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Redirect Info Model. +/// +internal class RedirectInfo +{ + /// + /// Gets or sets the redirect type. + /// + public RedirectType? RedirectType { get; set; } + + /// + /// Gets or sets a value indicating whether the query string should be preserved. + /// + public bool IsQueryStringPreserved { get; set; } + + /// + /// Gets or sets the pattern. + /// + public string? Pattern { get; set; } + + /// + /// Gets or sets the target. + /// + public string? Target { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectType.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectType.cs new file mode 100644 index 0000000..56532ad --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/RedirectType.cs @@ -0,0 +1,23 @@ +// ReSharper disable InconsistentNaming - Must match with string representation +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Redirect type enumeration. +/// +internal enum RedirectType +{ + /// + /// Permanent Redirect. + /// + REDIRECT_301, + + /// + /// Temporary Redirect. + /// + REDIRECT_302, + + /// + /// Server Transfer. + /// + SERVER_TRANSFER +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/Site.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/Site.cs new file mode 100644 index 0000000..8086bc0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/Site.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Site Model. +/// +internal class Site +{ + /// + /// Gets or sets the site info. + /// + public SiteInfo? SiteInfo { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfo.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfo.cs new file mode 100644 index 0000000..2a4d2ee --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfo.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Site Info Model. +/// +internal class SiteInfo +{ + /// + /// Gets or sets the site map. + /// + public string[]? Sitemap { get; set; } + + /// + /// Gets or sets the redirects. + /// + public RedirectInfo[]? Redirects { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfoResultModel.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfoResultModel.cs new file mode 100644 index 0000000..895186b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SiteInfoResultModel.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Site Info Result Model. +/// +internal class SiteInfoResultModel +{ + /// + /// Gets or sets the site. + /// + public Site? Site { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SitemapOptions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SitemapOptions.cs new file mode 100644 index 0000000..0ea6c17 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Models/SitemapOptions.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +/// +/// Additional options for Sitemap service. +/// +public class SitemapOptions +{ + /// + /// Gets or sets Url property needed for sitemap service. + /// + public Uri? Url { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Middleware/SitecoreRewriteMiddleware.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Middleware/SitecoreRewriteMiddleware.cs new file mode 100644 index 0000000..8ac845a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Middleware/SitecoreRewriteMiddleware.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; +using Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Models; +using Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Rules; +using Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Middleware; + +/// +/// AspNetCore middleware for Sitecore rewriting. +/// +internal class SitecoreRewriteMiddleware +{ + private const string RewriteOptionsCacheKey = "rewrite_options"; + private readonly RewriteOptions _staticRewriteOptions; + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IRedirectsService _redirectsService; + private readonly IMemoryCache _memoryCache; + private readonly SitecoreRewriteOptions _sitecoreRewriteOptions; + + /// + /// Initializes a new instance of the class. + /// + /// Next middleware to execute. + /// Logger factory to use. + /// Redirect service to use. + /// Memory cache to use. + /// Rewrite configuration Options. + /// Static rewrite configuration options. + public SitecoreRewriteMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IRedirectsService redirectsService, + IMemoryCache memoryCache, + IOptions rewriteOptions, + IOptions staticRewriteOptions) + { + ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(redirectsService); + + _next = next; + _redirectsService = redirectsService; + _logger = loggerFactory.CreateLogger(); + _memoryCache = memoryCache; + _sitecoreRewriteOptions = rewriteOptions.Value; + _staticRewriteOptions = staticRewriteOptions.Value; + } + + /// + /// Executes the middleware. + /// + /// Context data. + /// for the execution. + /// When context is null. + public async Task InvokeAsync(HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + context.TryGetResolvedSiteName(out string? resolvedSiteName); + + RewriteOptions? options = await _memoryCache.GetOrCreateAsync($"{RewriteOptionsCacheKey}_{resolvedSiteName}", async cacheEntry => + { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_sitecoreRewriteOptions.CacheTimeout); + return await GetSitecoreRewriteOptionsAsync(resolvedSiteName).ConfigureAwait(false); + }).ConfigureAwait(false); + + RewriteContext rewriteContext = new() + { + HttpContext = context, + StaticFileProvider = options!.StaticFileProvider, + Logger = _logger, + Result = RuleResult.ContinueRules + }; + + RunRules(rewriteContext, options, context, _logger); + if (rewriteContext.Result == RuleResult.EndResponse) + { + return; + } + + await _next(context).ConfigureAwait(false); + } + + private static void RunRules(RewriteContext rewriteContext, RewriteOptions options, HttpContext httpContext, ILogger logger) + { + foreach (IRule rule in options.Rules) + { + rule.ApplyRule(rewriteContext); + switch (rewriteContext.Result) + { + case RuleResult.ContinueRules: + logger.LogDebug("Request is continuing in applying rules. Current url is {EncodedUrl}", httpContext.Request.GetEncodedUrl()); + break; + case RuleResult.EndResponse: + logger.LogDebug("Request is done processing. Location header '{LocationHeader}' with status code '{StatusCode}'.", httpContext.Response.Headers.Location, httpContext.Response.StatusCode); + return; + case RuleResult.SkipRemainingRules: + logger.LogDebug("Request is done applying rules. Url was rewritten to {EncodedUrl}", httpContext.Request.GetEncodedUrl()); + return; + default: + throw new ArgumentOutOfRangeException($"Invalid rule termination {rewriteContext.Result}"); + } + } + } + + private async Task GetSitecoreRewriteOptionsAsync(string? siteName) + { + RewriteOptions options = new(); + RedirectInfo[]? redirectsResult = await _redirectsService.GetRedirects(siteName).ConfigureAwait(false); + if (redirectsResult != null && redirectsResult.Length != 0) + { + foreach (RedirectInfo redirectInfo in redirectsResult) + { + if (redirectInfo.RedirectType != null && !string.IsNullOrWhiteSpace(redirectInfo.Pattern) && !string.IsNullOrWhiteSpace(redirectInfo.Target)) + { + bool isRegex = redirectInfo.Pattern.StartsWith('^') && redirectInfo.Pattern.EndsWith('$'); + string formattedRegex = $"^{(isRegex ? redirectInfo.Pattern.TrimStart('^').TrimEnd('$').Trim('/') : redirectInfo.Pattern.Trim('/'))}[/]?$"; + + switch (redirectInfo.RedirectType) + { + case RedirectType.SERVER_TRANSFER: + options.Add(new SitecoreRewriteRule(formattedRegex, $"{redirectInfo.Target}", true, redirectInfo.IsQueryStringPreserved)); + break; + case RedirectType.REDIRECT_301: + options.Add(new SitecoreRedirectRule(formattedRegex, $"{redirectInfo.Target}", 301, redirectInfo.IsQueryStringPreserved)); + break; + case RedirectType.REDIRECT_302: + options.Add(new SitecoreRedirectRule(formattedRegex, $"{redirectInfo.Target}", 302, redirectInfo.IsQueryStringPreserved)); + break; + } + } + } + } + + if (_staticRewriteOptions.Rules.Any()) + { + foreach (IRule rule in _staticRewriteOptions.Rules) + { + options.Add(rule); + } + } + + return options; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Models/SitecoreRewriteOptions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Models/SitecoreRewriteOptions.cs new file mode 100644 index 0000000..bb2d53e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Models/SitecoreRewriteOptions.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Models; + +/// +/// SitecoreRewriteOptions class represents options for SitecoreRewrite form appsettings.json file. +/// +public class SitecoreRewriteOptions +{ + /// + /// Gets SitecoreRewrite options name. + /// + public const string Name = "SitecoreRewrite"; + + /// + /// Gets or sets cache timeout in seconds for getting redirects from the sitecore. + /// + public int CacheTimeout { get; set; } = 60; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRedirectRule.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRedirectRule.cs new file mode 100644 index 0000000..f19f2f4 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRedirectRule.cs @@ -0,0 +1,129 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.Logging; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Rules; + +/// +/// Rule that redirects the request when the Url matches a regular expression. +/// +internal class SitecoreRedirectRule : IRule +{ + private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(1); + + /// + /// Initializes a new instance of the class. + /// + /// Regular Expression to match. + /// Replacement value for matches. + /// Status code to respond with. + /// Where the query string is retained after processing. + public SitecoreRedirectRule(string regex, string replacement, int statusCode, bool isQueryStringPreserved) + { + ArgumentException.ThrowIfNullOrWhiteSpace(regex); + ArgumentException.ThrowIfNullOrWhiteSpace(replacement); + + InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, _regexTimeout); + Replacement = replacement; + StatusCode = statusCode; + IsQueryStringPreserved = isQueryStringPreserved; + } + + /// + /// Gets Compiled, CultureInvariant, IgnoreCase Regular Expression used by the rule. + /// + public Regex InitialMatch { get; } + + /// + /// Gets Replacement value for matches. + /// + public string Replacement { get; } + + /// + /// Gets the status code to respond with. + /// + public int StatusCode { get; } + + /// + /// Gets a value indicating whether the query string is retained after processing. + /// + public bool IsQueryStringPreserved { get; } + + /// + public void ApplyRule(RewriteContext context) + { + ArgumentNullException.ThrowIfNull(context); + + HttpRequest request = context.HttpContext.Request; + PathString path = request.Path; + PathString pathBase = request.PathBase; + + Match initMatchResults = InitialMatch.Match(!path.HasValue ? string.Empty : path.ToString()[1..]); + + if (initMatchResults.Success) + { + string newPath = initMatchResults.Result(Replacement); + HttpResponse response = context.HttpContext.Response; + + response.StatusCode = StatusCode; + context.Result = RuleResult.EndResponse; + + string encodedPath; + + if (string.IsNullOrEmpty(newPath)) + { + encodedPath = pathBase.HasValue ? pathBase.Value : "/"; + } + else + { + HostString host = default; + int schemeSplit = newPath.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + string scheme = request.Scheme; + + if (schemeSplit >= 0) + { + scheme = newPath[..schemeSplit]; + schemeSplit += Uri.SchemeDelimiter.Length; + int pathSplit = newPath.IndexOf('/', schemeSplit); + + if (pathSplit == -1) + { + host = new HostString(newPath[schemeSplit..]); + newPath = "/"; + } + else + { + host = new HostString(newPath[schemeSplit..pathSplit]); + newPath = newPath[pathSplit..]; + } + } + + if (newPath[0] != '/') + { + newPath = '/' + newPath; + } + + QueryString resolvedQuery = IsQueryStringPreserved ? request.QueryString : QueryString.Empty; + string resolvedPath = newPath; + + int querySplit = newPath.IndexOf('?', StringComparison.CurrentCultureIgnoreCase); + if (querySplit >= 0) + { + resolvedQuery = IsQueryStringPreserved ? request.QueryString.Add(QueryString.FromUriComponent(newPath[querySplit..])) : QueryString.FromUriComponent(newPath[querySplit..]); + resolvedPath = newPath[..querySplit]; + } + + encodedPath = host.HasValue + ? UriHelper.BuildAbsolute(scheme, host, pathBase, resolvedPath, resolvedQuery) + : UriHelper.BuildRelative(pathBase, resolvedPath, resolvedQuery); + } + + // Not using the HttpContext.Response.redirect here because status codes may be 301, 302, 307, 308 + response.Headers.Location = encodedPath; + + context.Logger.LogInformation("Request was redirected to {NewPath}", newPath); + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRewriteRule.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRewriteRule.cs new file mode 100644 index 0000000..0285e79 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Redirects/Rules/SitecoreRewriteRule.cs @@ -0,0 +1,112 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.Logging; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Redirects.Rules; + +/// +/// Rule that rewrites the Url according to a regular expression. +/// +internal class SitecoreRewriteRule : IRule +{ + private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(1); + + /// + /// Initializes a new instance of the class. + /// + /// Regular Expression to match. + /// Replacement value for matches. + /// Whether to stop processing any additional rules or continue. + /// Where the query string is retained after processing. + public SitecoreRewriteRule(string regex, string replacement, bool stopProcessing, bool isQueryStringPreserved) + { + ArgumentException.ThrowIfNullOrWhiteSpace(regex); + ArgumentException.ThrowIfNullOrWhiteSpace(replacement); + + InitialMatch = new Regex(regex, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, _regexTimeout); + Replacement = replacement; + StopProcessing = stopProcessing; + IsQueryStringPreserved = isQueryStringPreserved; + } + + /// + /// Gets Compiled, CultureInvariant, IgnoreCase Regular Expression used by the rule. + /// + public Regex InitialMatch { get; } + + /// + /// Gets Replacement value for matches. + /// + public string Replacement { get; } + + /// + /// Gets a value indicating whether to stop processing additional rules after this one. + /// + public bool StopProcessing { get; } + + /// + /// Gets a value indicating whether the query string is retained after processing. + /// + public bool IsQueryStringPreserved { get; } + + /// + public void ApplyRule(RewriteContext context) + { + ArgumentNullException.ThrowIfNull(context); + + PathString path = context.HttpContext.Request.Path; + Match initMatchResults = InitialMatch.Match(path == PathString.Empty ? path.ToString() : path.ToString()[1..]); + + if (initMatchResults.Success) + { + string result = initMatchResults.Result(Replacement); + HttpRequest request = context.HttpContext.Request; + + if (StopProcessing) + { + context.Result = RuleResult.SkipRemainingRules; + } + + if (string.IsNullOrEmpty(result)) + { + result = "/"; + } + + if (!IsQueryStringPreserved) + { + request.QueryString = QueryString.Empty; + } + + if (result.Contains(Uri.SchemeDelimiter, StringComparison.Ordinal)) + { + UriHelper.FromAbsolute(result, out string? scheme, out HostString host, out PathString pathString, out QueryString query, out _); + + request.Scheme = scheme; + request.Host = host; + request.Path = pathString; + request.QueryString = query.Add(request.QueryString); + } + else + { + int split = result.IndexOf('?', StringComparison.InvariantCultureIgnoreCase); + if (split >= 0) + { + string newPath = result[..split]; + request.Path = newPath[0] == '/' ? PathString.FromUriComponent(newPath) : PathString.FromUriComponent('/' + newPath); + + request.QueryString = request.QueryString.Add( + QueryString.FromUriComponent( + result[split..])); + } + else + { + request.Path = result[0] == '/' ? PathString.FromUriComponent(result) : PathString.FromUriComponent('/' + result); + } + } + + context.Logger.LogInformation("Request was rewritten to {Result}", result); + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs new file mode 100644 index 0000000..e8ab873 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs @@ -0,0 +1,134 @@ +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.GraphQL.Client.Models; +using Sitecore.AspNetCore.SDK.GraphQL.Exceptions; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +/// +/// Implements the Sitemap and Redirects services by GraphQl data retrieval from Edge or Preview Delivery API. +/// +/// Options to configure the GraphQl Client. +/// GraphQl Client. +/// Logger service. +internal class GraphQlSiteInfoService( + IOptions options, + IGraphQLClient graphQlClient, + ILogger logger) + : ISitemapService, IRedirectsService +{ + private const string SiteInfoQuerySitemap = """ + query SiteInfoQuery($site: String!) { + site { + siteInfo(site: $site) { + sitemap + } + } + } + """; + + private const string SiteInfoQueryRedirects = """ + query SiteInfoQuery($site: String!) { + site { + siteInfo(site: $site) { + redirects { + pattern + target + isQueryStringPreserved + redirectType + locale + } + } + } + } + """; + + private readonly SitecoreGraphQlClientOptions _options = options.Value; + + /// + public async Task GetSitemapUrl(string requestedUrl, string? siteName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(requestedUrl); + + siteName ??= _options.DefaultSiteName; + EnsureSiteName(siteName); + + GraphQLRequest siteInfoRequest = new() + { + Query = SiteInfoQuerySitemap, + OperationName = "SiteInfoQuery", + Variables = new + { + site = siteName + } + }; + + try + { + GraphQLResponse response = await graphQlClient.SendQueryAsync(siteInfoRequest).ConfigureAwait(false); + if (response.Errors != null && response.Errors.Length != 0) + { + foreach (GraphQLError graphQlError in response.Errors) + { + logger.LogError("[GraphQL Error] {Message}", graphQlError.Message); + } + } + + return response.Data.Site?.SiteInfo?.Sitemap?.FirstOrDefault(sitemap => sitemap.EndsWith(requestedUrl, StringComparison.InvariantCultureIgnoreCase))!; + } + catch (Exception exception) + { + logger.LogError("[GraphQL Client Error] {Message}", exception.Message); + + return string.Empty; + } + } + + /// + public async Task GetRedirects(string? siteName) + { + siteName ??= _options.DefaultSiteName; + EnsureSiteName(siteName); + + GraphQLRequest siteInfoRequest = new() + { + Query = SiteInfoQueryRedirects, + OperationName = "SiteInfoQuery", + Variables = new + { + site = siteName + } + }; + + try + { + GraphQLResponse response = await graphQlClient.SendQueryAsync(siteInfoRequest).ConfigureAwait(false); + + if (response.Errors != null && response.Errors.Length != 0) + { + foreach (GraphQLError graphQlError in response.Errors) + { + logger.LogError("[GraphQL Error] {Message}", graphQlError.Message); + } + } + + return response.Data.Site?.SiteInfo?.Redirects; + } + catch (Exception exception) + { + logger.LogError("[GraphQL Client Error] {Message}", exception.Message); + return null; + } + } + + private static void EnsureSiteName(string? siteName) + { + if (string.IsNullOrWhiteSpace(siteName)) + { + throw new InvalidGraphQlConfigurationException("Empty DefaultSiteName, provided in GraphQLClientOptions."); + } + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/IRedirectsService.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/IRedirectsService.cs new file mode 100644 index 0000000..f552859 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/IRedirectsService.cs @@ -0,0 +1,16 @@ +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +/// +/// Redirect Service Interface. +/// +internal interface IRedirectsService +{ + /// + /// Get the redirects for a named site. + /// + /// Site name. + /// Redirect info of the Site. + public Task GetRedirects(string? siteName); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/ISitemapService.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/ISitemapService.cs new file mode 100644 index 0000000..3722c2f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/ISitemapService.cs @@ -0,0 +1,15 @@ +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +/// +/// Sitemap Service Interface. +/// +public interface ISitemapService +{ + /// + /// Get the Sitemap Url for the requested url and named site. + /// + /// Requested Url. + /// Site name. + /// Url in string representation to display the relevant sitemap. + public Task GetSitemapUrl(string requestedUrl, string? siteName); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/SitemapService.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/SitemapService.cs new file mode 100644 index 0000000..8bfec0e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/SitemapService.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; + +namespace Sitecore.AspNetCore.SDK.SearchOptimization.Services; + +/// +/// Implements Sitemap service to apply configuration options to Urls. +/// +/// Sitemap Options. +internal class SitemapService(IOptions options) + : ISitemapService +{ + private readonly SitemapOptions _options = options.Value; + + /// + public Task GetSitemapUrl(string requestedUrl, string? siteName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(requestedUrl); + + if (_options.Url != null) + { + string finalUrl = new Uri(_options.Url, requestedUrl).ToString(); + return Task.FromResult(finalUrl); + } + + return Task.FromResult(string.Empty); + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Sitecore.AspNetCore.SDK.SearchOptimization.csproj b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Sitecore.AspNetCore.SDK.SearchOptimization.csproj new file mode 100644 index 0000000..d8191c6 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Sitecore.AspNetCore.SDK.SearchOptimization.csproj @@ -0,0 +1,33 @@ + + + + Sitecore Rendering Host + .NET SDK for creating a Sitecore Headless Rendering Host with SEO capabilities + + + + + + + + + + + + + + + + + + <_Parameter1>Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests + + + + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/DateTimeProvider.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/DateTimeProvider.cs new file mode 100644 index 0000000..9ced8eb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/DateTimeProvider.cs @@ -0,0 +1,11 @@ +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.Providers; + +/// +public class DateTimeProvider : IDateTimeProvider +{ + /// + public DateTime GetUtcNow() + { + return DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/IDateTimeProvider.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/IDateTimeProvider.cs new file mode 100644 index 0000000..d7e1c25 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Providers/IDateTimeProvider.cs @@ -0,0 +1,13 @@ +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.Providers; + +/// +/// DateTimeProvider - abstract realization for DateTime. +/// +public interface IDateTimeProvider +{ + /// + /// Gets UtcNow DateTime. + /// + /// UtcNow DateTime . + DateTime GetUtcNow(); +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.csproj b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.csproj new file mode 100644 index 0000000..0f0938e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.csproj @@ -0,0 +1,20 @@ + + + + Sitecore Rendering Host + .NET SDK for creating a Sitecore Headless Rendering Host with tracking capabilities + + + + + + + + + + + + + + + diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/SitecoreVisitorIdentificationOptions.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/SitecoreVisitorIdentificationOptions.cs new file mode 100644 index 0000000..c8d54bc --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/SitecoreVisitorIdentificationOptions.cs @@ -0,0 +1,13 @@ +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; + +/// +/// Options for Sitecore Visitor Identification functionality. +/// +public class SitecoreVisitorIdentificationOptions +{ + /// + /// Gets or sets Uri to Sitecore instance, which will serve proxied requests. + /// It is used to handle requests to Visitor identification pages located in [sitecore host name]/layouts/System. + /// + public Uri? SitecoreInstanceUri { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/TagHelpers/SitecoreVisitorIdentificationTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/TagHelpers/SitecoreVisitorIdentificationTagHelper.cs new file mode 100644 index 0000000..fff842e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/TagHelpers/SitecoreVisitorIdentificationTagHelper.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.Providers; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.TagHelpers; + +/// +/// This tag helper renders necessary artifacts for Visitor identification. +/// +[HtmlTargetElement(VisitorIdentificationConstants.TagHelpers.VisitorIdentificationHtmlTag)] +public class SitecoreVisitorIdentificationTagHelper : TagHelper +{ + private const string CookieName = "SC_ANALYTICS_GLOBAL_COOKIE"; + + /// + /// Gets or sets view context. + /// + [ViewContext] + public ViewContext? ViewContext { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(output); + + IOptions? options = ViewContext?.HttpContext.RequestServices.GetService>(); + IDateTimeProvider? dateTimeProvider = ViewContext?.HttpContext.RequestServices.GetService(); + + if (options == null || options.Value.SitecoreInstanceUri == null) + { + output.SuppressOutput(); + return; + } + + bool isVisitorClassified = IsVisitorIdentified(); + + if (!isVisitorClassified) + { + output.TagName = string.Empty; + output.Content.AppendHtml($"\"/>"); + output.Content.AppendHtml(""); + output.Content.AppendHtml(""); + return; + } + + output.SuppressOutput(); + } + + private bool IsVisitorIdentifiedInRequestCookie() + { + return ViewContext!.HttpContext.Request.Cookies.Any(c => c.Key == CookieName && c.Value.Contains("true", StringComparison.OrdinalIgnoreCase)); + } + + private bool IsVisitorIdentified() + { + string? cookieStr = ViewContext!.HttpContext.Response.Headers.SetCookie.FirstOrDefault(c => c != null && c.Contains(CookieName, StringComparison.OrdinalIgnoreCase)); + + if (cookieStr == null) + { + return IsVisitorIdentifiedInRequestCookie(); + } + + if (cookieStr.Contains("True", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationAppConfigurationExtensions.cs new file mode 100644 index 0000000..c138c7d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationAppConfigurationExtensions.cs @@ -0,0 +1,72 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProxyKit; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.Providers; + +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; + +/// +/// Sitecore Tracking Visitor identification configurations. +/// +public static class VisitorIdentificationAppConfigurationExtensions +{ + /// + /// Configures application to use sitecore tracking visitor identification. In case of global configuration of RenderingEngine + /// must be executed before . + /// + /// Application builder. + /// Modified application builder. + public static IApplicationBuilder UseSitecoreVisitorIdentification(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + if (app.ApplicationServices.GetService(typeof(IOptions)) is not IOptions options) + { + return app; + } + + if (options.Value.SitecoreInstanceUri != null) + { + app.Map("/layouts/System", api => + { + api.RunProxy(async context => + { + Uri finalUrl = new(options.Value.SitecoreInstanceUri, context.Request.PathBase.ToString()); + IPAddress? ip = context.Connection.RemoteIpAddress; + + ForwardContext? forwardContext = context.ForwardTo(finalUrl); + forwardContext.UpstreamRequest.Headers.ApplyXForwardedHeaders(ip, context.Request.Host, context.Request.Scheme); + + return await forwardContext.Send().ConfigureAwait(false); + }); + }); + } + + return app; + } + + /// + /// Configuration of Sitecore Visitor Identification functionality. + /// + /// The to add services to. + /// Sitecore Tracking options configuration. + /// The so that additional calls can be chained. + public static IServiceCollection AddSitecoreVisitorIdentification(this IServiceCollection services, Action? options = null) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddProxy(); + + if (options != null) + { + services.Configure(options); + } + + services.TryAddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationConstants.cs b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationConstants.cs new file mode 100644 index 0000000..d64f52b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification/VisitorIdentificationConstants.cs @@ -0,0 +1,20 @@ +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.TagHelpers; + +namespace Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; + +/// +/// Constants for tracking logic. +/// +internal static class VisitorIdentificationConstants +{ + /// + /// Tag helpers section. + /// + internal static class TagHelpers + { + /// + /// The HTML tag used by the tag helper. + /// + internal const string VisitorIdentificationHtmlTag = "sc-visitor-identification"; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Tracking/Sitecore.AspNetCore.SDK.Tracking.csproj b/src/Sitecore.AspNetCore.SDK.Tracking/Sitecore.AspNetCore.SDK.Tracking.csproj new file mode 100644 index 0000000..b7cfdc7 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking/Sitecore.AspNetCore.SDK.Tracking.csproj @@ -0,0 +1,12 @@ + + + + Sitecore Rendering Host + .NET SDK for creating a Sitecore Headless Rendering Host with tracking capabilities + + + + + + + diff --git a/src/Sitecore.AspNetCore.SDK.Tracking/TrackingAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Tracking/TrackingAppConfigurationExtensions.cs new file mode 100644 index 0000000..b9880bb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Tracking/TrackingAppConfigurationExtensions.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.Tracking; + +/// +/// Sitecore Tracking configurations. +/// +public static class TrackingAppConfigurationExtensions +{ + /// + /// Configuration of Sitecore tracking functionality. + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static ISitecoreRenderingEngineBuilder WithTracking(this ISitecoreRenderingEngineBuilder serviceBuilder) + { + ArgumentNullException.ThrowIfNull(serviceBuilder); + + serviceBuilder.ForwardHeaders(o => + { + o.HeadersWhitelist.Add("cookie"); + o.HeadersWhitelist.Add("set-cookie"); + o.HeadersWhitelist.Add("user-agent"); + o.HeadersWhitelist.Add("referer"); + }); + + serviceBuilder.Services.Configure(renderingOptions => + { + renderingOptions.MapToRequest((httpRequest, layoutRequest) => + { + Dictionary proxiedMetadata = new(StringComparer.OrdinalIgnoreCase); + + if (httpRequest.HttpContext.Connection.RemoteIpAddress != null) + { + string ip = httpRequest.HttpContext.Connection.RemoteIpAddress.ToString(); + + proxiedMetadata.Add("X-Forwarded-For", [ip]); + } + + layoutRequest.AddHeaders(proxiedMetadata); + }); + }); + + return serviceBuilder; + } +} \ No newline at end of file diff --git a/src/icon.png b/src/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..829b03ac2f3f7d61198bad417f8a99bf1301f62c GIT binary patch literal 8968 zcmV+jBlp~iP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>DBArP@K~#8N?Oh9e zRMnZEd*_*ighvb#UI7v$2?PisB$sg+c5U5OE7jWF?QUz!R_ofXU9DRM zAN8?TtE)xDUsWh?LXyclfP@gt1Ioh$V+8WfBy-RHzdI)blbJl`-a8=={=eVko^$Tx z&bjCN&i6RyJLd?xVTRI;8!e4CTddUB7^O29Vg!R>ChP1B!>vioZWqMBz*yGaE`OIX zCSM7HAm{|6QO7zus&smNo6g~=7R=@b{BMO$l8%c(L6yN)YuTJFTiW=)3=0f-2FOcH zjFVbh(?Q&LWD`Kid7vz)sqw23vj_si^$2psg2{A93mNYmx6Ph?C#?Dxkqmd1 zTmwf9)WSp7W5+(``2oRzF+hG&Qm~_;;U(s9+zpN&>Al`N7-KMk-_?hN{2(bmzmb>v z6$3&DTU^{CX1BkG6>#V9a9 zt}Q0!AztD)_)P*7uUZw*R$cu(#M=GWM7~1MbfqZ1-dR`o;F9`!zqpHjAANfoH$&2F13~^TFg7+TVaJXEah>w5Rue?>GBCq> zWbc4<80Wm>f8bqZCRkEerwJ2i!T@s54-6a_;*)q6gktoDSHN)ZSxZX?gy=n?WcBKx z))OagL!BPdIUI|jcKj|Dv}6L+l_i}I{px?QFG&&%we5znV>gdlv}pI7En6ghXV4%o zF)_h$@#3RkhQGtduR$x2eAT#Qq*Yd`VggkRK-Rut%$Q<-8+>^FXUuN@<>J$)i+RzY zM(+IiGo(M%Hg`1sR5&7cVBL@eUMMj{JOb_54bF#>(;c<}oW*t{y7HC!`!c zT%B4`@efdmiy#TIWVPi7P}}Q6LO#t54b}4^pP`rm+D@K)9%9))em-NZXuK!P)279z zo;a~{FpB&Bz5S3&s{3!optWIiI0O1<1p@ot>M(-Tp9mj_8Ae zzMWiB@;T2B3*;?W5bkJd+Kl8Hw41X~D>x^SerP>@oMu^lg3mR{OGrq=i|j+v=q%9) zj7j+X&J+=`DRIx9VYT?83pul9iK5y3u+-K@(N6)qSS#4^%#_sfa-SxUd}08a((Sx( z;Rt?6+ld`PXca{9yVkNYnlT*qu*JkAqSJl^hM&xfw1Fg58pe%V;LB99Pc{I}=W9)T zh^|`niX|h3_ymaRLe}1%fMl+-L?eh|l%uinC7$aS`Yi$ENS=<4kH7$$DKJ>EpMz^} zPOGl2;YA|~*^!YJF(BYWwCgRrNIl3NFdEnNH?pW-5`bd(~EM#LwBk97V>CVpOg{9K-N!;<=X2^RbYbda6ANG)iI z{AOBe$Mas%D+9}Os`HC)z15f<74=MKV`Fjd+_}%um;k@!LgN1Y zZ2{5I-$u_Z<3(y=ZOqBpz|&sQBLi^Gp#ckLff_I-f%xA|IeN5^my9SBE?qjIt*UA} z+TdUCF+i$5ijL)#2@*d2^eg?SQ8b&VN%)u4+`OqbDbOPWG-hQH=V-E4PcRs^SW8Ps zjGuocK;(G)xpOvjg5|u(2~m6mM1Bs$@t{F+Ny#zMY<_?`4lhy;L5RMzcW;kUz$*g~ z$G|t;#Ix$a*b(EziT}oPBMP?I*n2?$8$R9#&m`#)i0EJQoEu;#y$65(HJ()trv&d& z3V3AzTA#1U5^cs>#h{>X57)8%o=|l2&BmOVm}lURzlAoi@M2GZI1tf4$&QTl8vhRl z_-9EEy|<8MJ^X{x;>%pFCcK=29zj@ zAoL5_GiG><2BsW7+zgxHek4_^0=So6Y3jiMv@MG!mOo?V=BZQvi|0lJAoAnwXU}@- z`@X=KD40xJ!2}*IqP46n6O8mW&nkz*VYSVkJ)h@X@L&MilZWdKwRm*lu3aNW^rLa_ zh1%>{-}eQ^0%0s~%TG@B7{ZlWTK*OH)p-RJtjgFUJm-QN1JG_<&|7PHRt@%yzUijj zJUy&IBY5!dpMufcyT0!a1Yt(!H{ZMg&%vz+E5XB$yv z(sp}1C&P^aVEnn=R0PJhr=2`GWCA#qA}2a}Zd*mgel+STUZf4cMAymc+RwAv!f8_c2EBEZ zoafL72fq#`P!~NSimoZ=%Qk>se;ZFJ17ppmDN{z&ct4oE6Rz7MAc&n8X$!{2+s~f; z0nf=uE-5JmtqOQn8AQ=VYncJyd&cP;4%$;+IT+ivaOcjex={0Kz*<&TDws@f@~rj{ zgnunuy3|byNUg2v)BtEL(;Sc)z|q{C4z8f2V!(s=Kb{wO-<=?lK!yvb~N~PkB-)WSWbm z8#h{@XXeR83c(zX5#jpvhT!z{99+~--Fky48tgSSOL3}6X zNck`(DuK%Q#}yhvAddjvz+ZS)b3nzU@T3gGm@&B^q8htHAli%a>00WBv6}^>aY!GW zq!r)|XuH(w3P8og@}!JK+1ah2+i{*%1gW_>O2~d3h4gjc634 zr$=_w*3vewyu=Ae$SN@);0-xL_qy&&e9WvXPuH$80(oX6=JAo*+s~yOI`l_=M@G&K zH@uAhd4OkqhafyI*k;fE6AX%bc#-cA#Wz!rA9owF9LWGt8ql!L4G1~(K|yv%Z6dqiN%1p1lSD#O5guv=NBp z>Zyv`8_zL@g}HTBe_Ps8eHfeCU$b+?%kKSb7f4i*M%PMX{>jR zAt(TKV08>|4Q_x{PQ-Wb5rWWt+cw1>m*b+BgUMA$aFYJ&7yu8DT3UGK8anIi7j*BD z(EoWnNenVd=Ws;utj_?!G}X7V1W*&sP8@WN0WJdupxcBEK{T0M6GU*C=b_aVZxCr1 zGsevbgj*OOS}YTJRuRYz)HMkJpd-;{5IhGk)~OE)az#n!Cne2*{#O-wCE4w6 z5}>J|Af#*PSPAgIjD~{gQVgJ>U4d5|-=?m~;>bP)-Lnf}#_;a3>!6a!<+No37JgaNW&R&Dd4bGf$ z2%_j_13>?)YI~x+2)(2#tO%HkMo=Z%hB7Yg+Vw3^!fih#(QJN#CuIhK?*0YODhH$r zB)}z}>%szlRff*aWopddAM3|v3=CEKF-p7q!VldCECJ6m|B~r`2)d5Csgo|3*Vj z;8|+auVMNBnCEEZF9hn~S)Nsk=8$E}YI$1MR$fk{j%p?X7|~vwvt~;rZ`8-8-1^ur3 zsH5+YX)eD2dT_5>iuVavqfL2vQ+dt}IkRSId$`n9M!wZL1J*a~=jUB(v(Pm4j;r&d(Rp+4C&W`w|4`w#^3JW!gk zw;(P5ZFEM)GS~`B@tpoLLc16g-qz7dj=h<(}eW-J$nfE>B1Xtbc<*mDj+}J78COy=(&5)aO34da+@XT!@#+7SEW@~yNKWR#TVQ0 zuHJT8J%jJBf`Qm-i;aCI7yn0v{bdBfXnd6?=>W<(I)`Hs&ng3BuEo4A29WG_x0x#i z5FCw-ZgJ}baN0%mh%f9lHNWIVLk1vJ!lk`?3*gE@gAZil zLpp@EeEzvt;zLE8flA1LiufADUj;_`XMFE7Fx86%X=(CqUA-{^{=X2P9Xv~+;P<0F zRmgu3%4Bk#O5%DDZ5?Jmd-goIT=`C6jMZ3X&6=6;$tTUcLcA&VJVU%{NE!z1uyA@BH5n%~Pi??;<|j!u2TkdMbz;1R+9dYula^74;m{ zu}72@2zM{qP~9OOplv`*jGL|J!M$jdHy~!zE%%nBaxhce;=ldPO$wl=|2@RwyLed| zdw4G8@L@U;Lk8)+hRn>zSX*0)7#zG2;;>JZ0AfyTJ#k__bZfL|GR45SV)~E}3cdOY zdiiTKngv9h0auh7m@N1x^z=xiX&wqAML2i8fT~bridtfGC?5dAPFJHcf3`3r$aI+x1 z8+9Iahj$9Xs}TR+=Q$Z33_u4)KqTh5HIT0$wPvyW2;$6b?K*llg`ge8{Zuds0~7o< zc=6)L@UpH^DD<#c4Ldk??af8sM%3!^Xr9mAz=2&y;nAUJJ}`CtII6I{H$5q>Zm z|Df*F5J{>tUVr`UMcLVPyhH{h%pCNT5;Uf4WI?RG91t7(Be+zqi4IuIv!s@mS5e_9 z=-K~enl$OngdIEj#RzHvMlv*JWhFy`k%4yy>M)ky@&*Uq<26%w_`fIa-|w2Mhx@n@ zj7G*`|A{XMbhO^BV4}bB5*bieGa$`q3BTfSav6e<1LCLgoC{tV0KISndX5&`zWxy| z%zZrV0(!@<&^VV*B@=|*hLDhZk_rl3%yMYW8Uw^Gi7OgEXov6pNA$9{;YPZ`thGY% zEw@-YYHL&BcC7~E--XW<)QR8q0#X@*kO|51Q}kBX!jGbrE6wfI)xXBQALHYb2!(|D z1&IF?&&uEub0>b!wv7IV;bRIfxQfbbyHQ4sz=$@iyWU4Dm%H4V=rk?;F{NsPUY z-~K+H=Qbe&o9RAu#Q*KHUVT6i#qyAhj5$!XvTbrDdVDh6j=Q19-{D#1kfgI9R)V#x z%VP2>2)%v}7=cC`Nj>S&a3mSd;(sWp z6;@;)u+Ty5DQE12z+@}!sD14s`LVdp*mN5qizej3remZ37(yX~} zzUeY<$!h`Q{s4B%lX#wMAO3zrlC}pWCa$GZ#CeGf5Purph2k{So!|mcyQNiDzRR<2 z=&=Q4cZ@O8J?f+Yfw9jz>+4t30m-~X28g;Gy8bWdIqv$6u{xAJ1wHx_JPD7z@MPwJ z?+zm04#Lt&SVUasO|1Z;qYmZpO5T8^`G~fI<#$|weqIc<@f?Vlpyc;a_wD$;~8qW;{C%bxa2#XpWp#-;xIq1|tq{h`RSyf2x~P zr(eSVP#k{={x8!!eR}r7UAsJpPRLudXfa$%8r-2t9Yu{p#D_##n|}Vh8}XrjMX29b zP#1Y0A+_j9_jjS!%5z)bASz?!MQR|SGWVb|9>!(%j2Roj@y~+@9!wxZlA7UC{Y5ky zw_0wvVJAF>URR`|-a=bt<#kXoqdPBNoCHy7HjExU9`6y-`Hhp0jMrVK!~HXOZUY#s zDmXQ@ZqAD@_ULKkr=$d;Gu#1Y{U_9wwry41E1)vAQ~X5to==e5!f4dy*qt6M|0~hw zOF&E~OKoj7Bo)7~3yifGLPJ+4<>q=!2*LH-h|BZ%j&~;L0;va`ZXaH52cDH56dzyV z&A8D5c=upPp%wVhXx^=0#J&y1-av;zi{c9b(a}Fy@bSkUJA2q-V(x;w{z0E@&_6JU z{{-vt)T>@1w2X%2DHB<^o zYDLffF^u6?c!?Xbr%zu2$+8U$qD|a{AiRX$@AXXLE77kvgPsnB=%cAjbwiOHXy{rr z?((#gCq0mL8G_*cLt*oNL`(p^ygs?O*tLkF3t)orxc>^6;7-2~WT4;_`7h~5jrwVFQBe~wbwk$V$t%Qw z0PT7{L?D=cjrG{Ek9kS2@Tqm7r~iZ>&@RYR5L)oLH}&}OZM?(<8U=%K_8@u&jT(BK zr#t8&2o9w^z#>nMctz#S)F09A9-wS3)0W%ED3M4*i2wnRbnDtMA^XKoK_x9UfMSU0Y7Ay#d z)TCHGEso~*0P6A@^uD4EC=@e5@v2n;Z6{CeLWA@=(xd`4u#O*t{(sAxZEz)WmHnUxDi`i1}T{@bJvUJ$w4xAFpWTN(-I*iC+IW+DY}K zj#r3cwq@G16$v|bwDJ<4ph|?>=FADjixogGdxS)50;g?|17H1D!D(rGu8bV&4FyY< zggY7<7J(@u!4$F3RCF%VWOx)LdW~qb0QuSO=wLKhV=&a=yEJoIhIAGFCr=+3_<3?k zNuNG-7fSBD(?D^NQcKIv@vLZk_#JHl30&Fv%{K{C?9_+__WXmfNx}4s9J9SD`IK3t{%$htoLJSTrL8X*0Sm+O&5}^VN_Xgb2 zcajSV&hR@|74T4IcGlP53nu*@+92Goga;sTtHHb(i_e}_y_Q0=nnLU*b=20l3>)!jeP8TNeyjaG(FrC_nt;d(Y8CZ?g` zz=5-1RGJ8qzv^gglsQ~v(d06JBr)ZDyW ztx2Avj zrMgvHUf>s&UQ%D*V3|Ih0x4c|wd`O32u=i+iF~E>1~a(I2NS661mU%SS+iF7gZKo$ z>#&@tsE2e0!~fuAhSgp{0{}Z1k^)G49^Ul@erEuJJb~2M_!hWX%UQ(X3)n2MJvR6{ zv7-b7V%1R42N-}(%Xnu{sf57>XWP7C7&mUgKoFlWAQIq8*k;eZ6CMJIwSgHg7&@qd zrTG6~!M|xbbGf)^b z-Hz5GN^NZ$&@h|OK*N#|p!Uk~j!z0E)B8QG)f;FGI=M>{p&>JK9Sp^XxoQ~Nxc-8TJ9j=9;SqdjZ(RRG)c6au*_J5254E-##+}=?EQ1L{ zqIVryw=STyv~)d~U_EmZYM@iZ(7>zrP*I&Q0YX{Duf}yQnfxN`TIM&{t4&y2w z*#r<|9ta|!7dpS76`aIoxE{f$ShQFU8ODx1n3$O<@47uyG2|JbJ77dw8f~^%sj)Fi zXE4MF2E$A+2<;#;1BA1{0*{1E;anVg1?1U4qmedaE<>6Ow{y5s3_%6He_RX-s-!b^ iV8nT>=^wh#>HZ&3^U?zA(#UK80000 + + + + + net8.0 + enable + enable + true + false + true + $(NoWarn),1573,1591,1712 + + + + + Tests.GlobalSuppressions.cs + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/ISetupActionsProvider.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/ISetupActionsProvider.cs new file mode 100644 index 0000000..ab3f508 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/ISetupActionsProvider.cs @@ -0,0 +1,8 @@ +using AutoFixture; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.ActionProviders; + +public interface ISetupActionsProvider +{ + IEnumerable> GetSetupActions(Type? type, string fixtureAction); +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/SetupActionsProvider.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/SetupActionsProvider.cs new file mode 100644 index 0000000..cfce8b0 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/SetupActionsProvider.cs @@ -0,0 +1,37 @@ +using AutoFixture; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.ActionProviders; + +public abstract class SetupActionsProvider : ISetupActionsProvider +{ + public virtual IEnumerable> GetSetupActions(Type? type, string fixtureAction) + { + object? setup = ResolveFromType(type, fixtureAction); + + return ResolveActions(setup); + } + + protected static IEnumerable> ResolveActions(object? actions) + { + if (actions == null) + { + return []; + } + + if (actions is IEnumerable> enumerable) + { + return enumerable; + } + + if (actions is not Action single) + { + throw new ArgumentNullException(nameof(actions), "No setup action provided"); + } + + enumerable = [single]; + + return enumerable; + } + + protected abstract object? ResolveFromType(Type? type, string fixtureAction); +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticMethodSetupActionsProvider.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticMethodSetupActionsProvider.cs new file mode 100644 index 0000000..c0a3cba --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticMethodSetupActionsProvider.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.ActionProviders; + +public class StaticMethodSetupActionsProvider : SetupActionsProvider +{ + protected override object? ResolveFromType(Type? type, string fixtureAction) + { + MethodInfo? method = type?.GetMethod( + fixtureAction, + BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + + return method?.Invoke(null, []); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticPropertySetupActionsProvider.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticPropertySetupActionsProvider.cs new file mode 100644 index 0000000..067926c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/ActionProviders/StaticPropertySetupActionsProvider.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.ActionProviders; + +public class StaticPropertySetupActionsProvider : SetupActionsProvider +{ + protected override object? ResolveFromType(Type? type, string fixtureAction) + { + PropertyInfo? property = type?.GetProperty( + fixtureAction, + BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + + return property?.GetValue(null); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoNSubstituteDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoNSubstituteDataAttribute.cs new file mode 100644 index 0000000..0167342 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoNSubstituteDataAttribute.cs @@ -0,0 +1,25 @@ +using AutoFixture; +using AutoFixture.AutoNSubstitute; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public class AutoNSubstituteDataAttribute : AutoSetupDataAttribute +{ + public AutoNSubstituteDataAttribute(params string[] fixtureSetups) + : base(fixtureSetups) + { + } + + public AutoNSubstituteDataAttribute(Type externalClassSource, params string[] fixtureSetups) + : base(externalClassSource, fixtureSetups) + { + } + + protected override IEnumerable> GetSetups(Type? functionSourceType) + { + return + new Action[] { x => x.Customize(new AutoNSubstituteCustomization { ConfigureMembers = true }) }.Concat( + base.GetSetups(functionSourceType)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoSetupDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoSetupDataAttribute.cs new file mode 100644 index 0000000..6b3f8ca --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/AutoSetupDataAttribute.cs @@ -0,0 +1,149 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Sitecore.AspNetCore.SDK.AutoFixture.ActionProviders; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public class AutoSetupDataAttribute : AutoDataAttribute +{ + public const string DefaultFixtureSetupName = "AutoSetup"; + + public const string AutoSetupExternalSourceFieldName = "AutoSetupSource"; + + private readonly string[] _fixtureSetups; + + private readonly Type? _classSource; + + private readonly IEnumerable _setupActionsProviders; + + /// + /// Defines that this test will have its parameter values generated by an AutoFixture instance, which can be + /// configured by static methods on this fixture class. + /// + /// The names of the public static methods that return of . 'AutoSetup' is inserted as the first name if it is not specified. + public AutoSetupDataAttribute(params string[] fixtureSetups) + : this(null, fixtureSetups) + { + } + + /// + /// Defines that this test will have its parameter values generated by an AutoFixture instance, which can be + /// configured by public static methods on a specified class. + /// + /// The class containing the public static methods to configure the instance. + /// The names of the public static methods that return of . 'AutoSetup' is inserted as the first name if it is not specified. + public AutoSetupDataAttribute(Type? externalClassSource, params string[] fixtureSetups) + : this( + [ + new StaticMethodSetupActionsProvider(), + new StaticPropertySetupActionsProvider() + ], + externalClassSource, + fixtureSetups) + { + } + + protected AutoSetupDataAttribute( + IEnumerable setupActionsProviders, + Type? externalClassSource, + params string[] fixtureSetups) + : base(() => new Fixture()) + { + if (fixtureSetups.Length == 0) + { + fixtureSetups = [DefaultFixtureSetupName]; + } + + if (!fixtureSetups.Contains(DefaultFixtureSetupName)) + { + fixtureSetups = new[] { DefaultFixtureSetupName }.Concat(fixtureSetups).ToArray(); + } + + _fixtureSetups = fixtureSetups; + _setupActionsProviders = setupActionsProviders; + _classSource = externalClassSource; + } + + public override IEnumerable GetData(MethodInfo testMethod) + { + Type? finalClassSourceType = + _classSource ?? + GetActionSourceTypeField(testMethod.ReflectedType, AutoSetupExternalSourceFieldName) ?? + GetActionSourceTypeProperty(testMethod.ReflectedType, AutoSetupExternalSourceFieldName) ?? + testMethod.ReflectedType; + + foreach (Action action in GetSetups(finalClassSourceType)) + { + // We currently need to use this member as we configure the fixture based on the method under test + // we use the given class to locate the setup members but if null, we use the type from the method under test +#pragma warning disable 0618 + action(Fixture); +#pragma warning restore 0618 + } + + return base.GetData(testMethod); + } + + protected virtual IEnumerable> GetSetups(Type? functionSourceType) + { + List> setupActions = []; + + foreach (string fixtureSetup in _fixtureSetups.Where(a => !string.IsNullOrWhiteSpace(a))) + { + List> setups = _setupActionsProviders + .SelectMany(p => p.GetSetupActions(functionSourceType, fixtureSetup)) + .ToList(); + + if (setups.Count == 0 && !fixtureSetup.Equals(DefaultFixtureSetupName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentOutOfRangeException(fixtureSetup, "No static property, method or field could be found on the test fixture with the name " + fixtureSetup); + } + + setupActions.AddRange(setups); + } + + return setupActions; + } + + private static Type? GetActionSourceTypeField(Type? type, string fieldName) + { + FieldInfo? member = type?.GetField( + fieldName, + BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + + if (member == null) + { + return null; + } + + if (member.GetValue(null) is not Type sourceType) + { + throw new ArgumentOutOfRangeException( + $"Field {fieldName} on {type?.FullName} did not return a Type value"); + } + + return sourceType; + } + + private static Type? GetActionSourceTypeProperty(Type? type, string fieldName) + { + PropertyInfo? member = type?.GetProperty( + fieldName, + BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + + if (member == null) + { + return null; + } + + if (member.GetValue(null) is not Type sourceType) + { + throw new ArgumentOutOfRangeException( + $"Property {fieldName} on {type?.FullName} did not return a Type value"); + } + + return sourceType; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoNSubstituteDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoNSubstituteDataAttribute.cs new file mode 100644 index 0000000..3ab9df3 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoNSubstituteDataAttribute.cs @@ -0,0 +1,20 @@ +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class InlineAutoNSubstituteDataAttribute : InlineAutoSetupDataAttribute +{ + public InlineAutoNSubstituteDataAttribute(Type externalClassSource, string[] fixtureSetups, params object[] values) + : base(new AutoNSubstituteDataAttribute(externalClassSource, fixtureSetups), values) + { + } + + public InlineAutoNSubstituteDataAttribute(string[] fixtureSetups, params object[] values) + : base(new AutoNSubstituteDataAttribute(fixtureSetups), values) + { + } + + public InlineAutoNSubstituteDataAttribute(params object[] values) + : base(new AutoNSubstituteDataAttribute(), values) + { + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoSetupDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoSetupDataAttribute.cs new file mode 100644 index 0000000..628c0dc --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/InlineAutoSetupDataAttribute.cs @@ -0,0 +1,28 @@ +using AutoFixture.Xunit2; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class InlineAutoSetupDataAttribute(AutoDataAttribute baseAttribute, params object[] values) + : InlineAutoDataAttribute(baseAttribute, values) +{ + public InlineAutoSetupDataAttribute(Type externalClassSource, string[] fixtureSetups, params object[] values) + : this(new AutoSetupDataAttribute(externalClassSource, fixtureSetups), values) + { + } + + public InlineAutoSetupDataAttribute(Type externalClassSource, params object[] values) + : this(new AutoSetupDataAttribute(externalClassSource), values) + { + } + + public InlineAutoSetupDataAttribute(string[] fixtureSetups, params object[] values) + : this(new AutoSetupDataAttribute(fixtureSetups), values) + { + } + + public InlineAutoSetupDataAttribute(params object[] values) + : this(new AutoSetupDataAttribute(), values) + { + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoNSubstituteDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoNSubstituteDataAttribute.cs new file mode 100644 index 0000000..1de6957 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoNSubstituteDataAttribute.cs @@ -0,0 +1,29 @@ +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class MemberAutoNSubstituteDataAttribute : MemberAutoSetupDataAttribute +{ + public MemberAutoNSubstituteDataAttribute(string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoNSubstituteDataAttribute(values); + } + + public MemberAutoNSubstituteDataAttribute(string[] fixtureSetups, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoNSubstituteDataAttribute(fixtureSetups, values); + } + + public MemberAutoNSubstituteDataAttribute(Type externalClassSource, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoNSubstituteDataAttribute(externalClassSource, values); + } + + public MemberAutoNSubstituteDataAttribute(Type externalClassSource, string[] fixtureSetups, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoNSubstituteDataAttribute(externalClassSource, fixtureSetups, values); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoSetupDataAttribute.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoSetupDataAttribute.cs new file mode 100644 index 0000000..5b9384c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Attributes/MemberAutoSetupDataAttribute.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using System.Reflection; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.Attributes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class MemberAutoSetupDataAttribute : MemberDataAttributeBase +{ + public MemberAutoSetupDataAttribute(string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoSetupDataAttribute(values); + } + + public MemberAutoSetupDataAttribute(string[] fixtureSetups, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoSetupDataAttribute(fixtureSetups, values); + } + + public MemberAutoSetupDataAttribute(Type externalClassSource, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoSetupDataAttribute(externalClassSource, values); + } + + public MemberAutoSetupDataAttribute(Type externalClassSource, string[] fixtureSetups, string memberName, params object[] parameters) + : base(memberName, parameters) + { + AttrFactory = values => new InlineAutoSetupDataAttribute(externalClassSource, fixtureSetups, values); + } + + protected Func AttrFactory { get; set; } + + public override IEnumerable GetData(MethodInfo testMethod) + { + foreach (InlineAutoSetupDataAttribute attr in base.GetData(testMethod).Select(AttrFactory)) + { + foreach (object[]? parameters in attr.GetData(testMethod)) + { + yield return parameters; + } + } + } + + protected override object[]? ConvertDataItem(MethodInfo testMethod, object? item) + { + if (item == null) + { + return null; + } + + if (item is not object[] array) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Property {0} on {1} yielded an item that is not an object[]", MemberName, MemberType ?? testMethod.DeclaringType)); + } + + return array; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Extensions/GuardClauseAssertionExtensions.cs b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Extensions/GuardClauseAssertionExtensions.cs new file mode 100644 index 0000000..9d7df4a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Extensions/GuardClauseAssertionExtensions.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using AutoFixture.Idioms; + +namespace Sitecore.AspNetCore.SDK.AutoFixture.Extensions; + +public static class GuardClauseAssertionExtensions +{ + public static void Verify(this GuardClauseAssertion assertion) + { + ArgumentNullException.ThrowIfNull(assertion); + assertion.Verify(typeof(T)); + } + + public static void VerifyConstructors(this GuardClauseAssertion assertion) + { + ArgumentNullException.ThrowIfNull(assertion); + assertion.Verify(typeof(T).GetTypeInfo().DeclaredConstructors); + } + + public static void VerifyMethod(this GuardClauseAssertion assertion, string methodName) + { + ArgumentNullException.ThrowIfNull(assertion); + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentNullException(nameof(methodName)); + } + + assertion.Verify(typeof(T).GetTypeInfo().GetDeclaredMethods(methodName)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.AutoFixture/Sitecore.AspNetCore.SDK.AutoFixture.csproj b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Sitecore.AspNetCore.SDK.AutoFixture.csproj new file mode 100644 index 0000000..4eb1227 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.AutoFixture/Sitecore.AspNetCore.SDK.AutoFixture.csproj @@ -0,0 +1,19 @@ + + + + false + + + + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Configuration/ExperienceEditorOptionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Configuration/ExperienceEditorOptionsFixture.cs new file mode 100644 index 0000000..274c35f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Configuration/ExperienceEditorOptionsFixture.cs @@ -0,0 +1,16 @@ +using AutoFixture.Xunit2; +using FluentAssertions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Configuration; + +public class ExperienceEditorOptionsFixture +{ + [Theory] + [AutoData] + public void Ctor_Assets_SetsDefaultValue([NoAutoProperties] ExperienceEditorOptions sut) + { + sut.Endpoint.Should().NotBeNullOrEmpty(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs new file mode 100644 index 0000000..2aa7b10 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Middleware; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Extensions; + +public class ApplicationBuilderExtensionsFixture +{ + [Fact] + public void UseSitecoreRenderingEngine_NullApplicationBuilder_Throws() + { + Func act = + () => ExperienceEditorAppConfigurationExtensions.UseSitecoreExperienceEditor(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void UseExperienceEditor_WithAppBuilderAndWithoutExperienceEditorServices_DoesNotCallMiddleware(IApplicationBuilder appBuilder) + { + // Arrange + appBuilder.ApplicationServices.GetService(typeof(ExperienceEditorMarkerService)).Returns(null); + + // Act + appBuilder.UseSitecoreExperienceEditor(); + + // Assert + bool received = appBuilder.ReceivedCalls().Any(c => c.GetArguments().OfType().Any(d => + d.Target?.GetType().GetField("_middleware", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(d.Target).As().FullName == typeof(ExperienceEditorMiddleware).FullName)); + received.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ServiceCollectionExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ServiceCollectionExtensionsFixture.cs new file mode 100644 index 0000000..e7f6dd2 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Extensions/ServiceCollectionExtensionsFixture.cs @@ -0,0 +1,18 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Extensions; + +public class ServiceCollectionExtensionsFixture +{ + [Fact] + public void AddSitecoreRenderingEngine_IsGuarded() + { + Func act = + () => ExperienceEditorAppConfigurationExtensions.WithExperienceEditor(null!); + + act.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Mappers/SitecoreLayoutResponseMapperFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Mappers/SitecoreLayoutResponseMapperFixture.cs new file mode 100644 index 0000000..350ffab --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Mappers/SitecoreLayoutResponseMapperFixture.cs @@ -0,0 +1,69 @@ +using AutoFixture; +using AutoFixture.Idioms; +using AutoFixture.Xunit2; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Mappers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Mappers; + +public class SitecoreLayoutResponseMapperFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Register(s => new PathString("/" + s)); + IOptions? optionSub = f.Freeze>(); + ExperienceEditorOptions options = new(); + optionSub.Value.Returns(options); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Map_NullRequest_ThrowsException(IOptions options, SitecoreLayoutResponseContent content) + { + // Arrange + SitecoreLayoutResponseMapper sut = new(options); + + // Arrange + Action action = () => sut.MapRoute(content, "/", null!); + + // Act / Assert + action.Should().Throw().WithParameterName("request"); + } + + [Theory] + [AutoNSubstituteData] + public void Map_WithRequest_ReturnsMappedRequest( + IOptions options, + [Frozen] HttpRequest request, + [Frozen] SitecoreLayoutResponseContent content) + { + // Arrange + content.Sitecore!.Route!.DatabaseName = "master"; + options.Value.MapToRequest((sitecoreResponse, scPath, httpRequest) => + httpRequest.Path = scPath + sitecoreResponse.Sitecore?.Route?.DatabaseName); + SitecoreLayoutResponseMapper sut = new(options); + + // Act + string? result = sut.MapRoute(content, "/testString/", request); + + // Assert + result.Should().Be("/testString/master"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Middleware/ExperienceEditorMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Middleware/ExperienceEditorMiddlewareFixture.cs new file mode 100644 index 0000000..62d41be --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Middleware/ExperienceEditorMiddlewareFixture.cs @@ -0,0 +1,143 @@ +using System.Text; +using AutoFixture; +using AutoFixture.Idioms; +using AutoFixture.Xunit2; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Configuration; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Middleware; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Middleware; + +public class ExperienceEditorMiddlewareFixture +{ + private const string EeSampleRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/Sample\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + private const string EditRequest = + """{"id":"jss-sample-app","args":["/?sc_itemid=%7bcfdd7ba2-e646-5294-87fc-6fad34451a97%7d&sc_ee_fb=false&sc_lang=en","{\"sitecore\":{\"context\":{\"pageEditing\":true,\"user\":{\"domain\":\"sitecore\",\"name\":\"Admin\"},\"site\":{\"name\":\"jss-sample-app\"},\"pageState\":\"edit\",\"language\":\"en\",\"itemPath\":\"/graphql/sample-1\"},\"route\":{\"name\":\"sample-1\",\"displayName\":\"sample-1\",\"fields\":{\"pageTitle\":{\"value\":\"Sample 1 Page Title\",\"editable\":\"{\\\"contextItem\\\":{\\\"id\\\":\\\"cfdd7ba2-e646-5294-87fc-6fad34451a97\\\",\\\"version\\\":1,\\\"language\\\":\\\"en\\\",\\\"revision\\\":\\\"722e36f6799b4e229ee00a77a5c65332\\\"},\\\"fieldId\\\":\\\"eb294dc3-969b-59cd-a193-2b5f18db8776\\\",\\\"fieldType\\\":\\\"Single-Line Text\\\",\\\"fieldWebEditParameters\\\":{\\\"prevent-line-break\\\":\\\"true\\\"},\\\"commands\\\":[{\\\"click\\\":\\\"chrome:common:edititem({command:\\\\\\\"webedit:open\\\\\\\"})\\\",\\\"header\\\":\\\"Edit the related item\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/cubes.png\\\",\\\"disabledIcon\\\":\\\"/temp/cubes_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the related item in the Content Editor.\\\",\\\"type\\\":\\\"common\\\"},{\\\"click\\\":\\\"chrome:rendering:personalize({command:\\\\\\\"webedit:personalize\\\\\\\"})\\\",\\\"header\\\":\\\"Personalize\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/users_family.png\\\",\\\"disabledIcon\\\":\\\"/temp/users_family_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Create or edit personalization for this component.\\\",\\\"type\\\":\\\"sticky\\\"}],\\\"contextItemUri\\\":\\\"sitecore://master/{CFDD7BA2-E646-5294-87FC-6FAD34451A97}?lang=en&ver=1\\\",\\\"custom\\\":{},\\\"displayName\\\":\\\"Page Title\\\",\\\"expandedDisplayName\\\":null}Sample 1 Page Title\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"cfdd7ba2-e646-5294-87fc-6fad34451a97\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"5179e218-3df6-5af7-8147-d2d4c05da992\",\"templateId\":\"dfe73d70-9835-584e-b0f5-28c58ab064d7\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"name\":\"code\",\"type\":\"text/sitecore\",\"contents\":\"{\\\"contextItem\\\":{\\\"id\\\":\\\"cfdd7ba2-e646-5294-87fc-6fad34451a97\\\",\\\"version\\\":1,\\\"language\\\":\\\"en\\\",\\\"revision\\\":\\\"722e36f6799b4e229ee00a77a5c65332\\\"},\\\"placeholderKey\\\":\\\"jss-main\\\",\\\"placeholderMetadataKeys\\\":[\\\"jss-main\\\"],\\\"editable\\\":true,\\\"commands\\\":[{\\\"click\\\":\\\"chrome:placeholder:addControl\\\",\\\"header\\\":\\\"Add to here\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/add.png\\\",\\\"disabledIcon\\\":\\\"/temp/add_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Add a new rendering to the '{0}' placeholder.\\\",\\\"type\\\":\\\"\\\"},{\\\"click\\\":\\\"chrome:placeholder:editSettings\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/window_gear.png\\\",\\\"disabledIcon\\\":\\\"/temp/window_gear_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the placeholder settings.\\\",\\\"type\\\":\\\"\\\"}],\\\"contextItemUri\\\":\\\"sitecore://master/{CFDD7BA2-E646-5294-87FC-6FAD34451A97}?lang=en&ver=1\\\",\\\"custom\\\":{\\\"allowedRenderings\\\":[\\\"EF310C848E2655329A11DB68DCF37BEF\\\",\\\"E2FA6BCC80A05FC4B71BC05B597A4060\\\",\\\"F0ED8D3C014657EB83EFF8AA2A351BA6\\\",\\\"603EAA1A61D05B7185CA346D9B55FECC\\\"],\\\"editable\\\":\\\"true\\\"},\\\"displayName\\\":\\\"Main\\\",\\\"expandedDisplayName\\\":null}\",\"attributes\":{\"type\":\"text/sitecore\",\"chrometype\":\"placeholder\",\"kind\":\"open\",\"id\":\"jss_main\",\"key\":\"jss-main\",\"class\":\"scpm\",\"data-selectable\":\"true\"}},{\"name\":\"code\",\"type\":\"text/sitecore\",\"contents\":\"{\\\"contextItem\\\":{\\\"id\\\":\\\"27f322af-3d30-5f2f-ada2-dab630d35ec6\\\",\\\"version\\\":1,\\\"language\\\":\\\"en\\\",\\\"revision\\\":\\\"ad0b2a91d2c5484facf59feb6b49dff1\\\"},\\\"renderingId\\\":\\\"ef310c84-8e26-5532-9a11-db68dcf37bef\\\",\\\"renderingInstanceId\\\":\\\"{9157CC8C-5760-5114-9F2D-93CBE39B30DC}\\\",\\\"editable\\\":true,\\\"commands\\\":[{\\\"click\\\":\\\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={27F322AF-3D30-5F2F-ADA2-DAB630D35EC6})',null,false)\\\",\\\"header\\\":\\\"Edit Fields\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/pencil.png\\\",\\\"disabledIcon\\\":\\\"/temp/pencil_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:dummy\\\",\\\"header\\\":\\\"Separator\\\",\\\"icon\\\":\\\"\\\",\\\"disabledIcon\\\":\\\"\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":null,\\\"type\\\":\\\"separator\\\"},{\\\"click\\\":\\\"chrome:rendering:sort\\\",\\\"header\\\":\\\"Change position\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/document_size.png\\\",\\\"disabledIcon\\\":\\\"/temp/document_size_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Move component.\\\",\\\"type\\\":\\\"\\\"},{\\\"click\\\":\\\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={9157CC8C-5760-5114-9F2D-93CBE39B30DC},renderingId={EF310C84-8E26-5532-9A11-DB68DCF37BEF},id={27F322AF-3D30-5F2F-ADA2-DAB630D35EC6})',null,false)\\\",\\\"header\\\":\\\"Edit Experience Editor Options\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/clipboard_check_edit.png\\\",\\\"disabledIcon\\\":\\\"/temp/clipboard_check_edit_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the Experience Editor options for the component.\\\",\\\"type\\\":\\\"common\\\"},{\\\"click\\\":\\\"chrome:rendering:properties\\\",\\\"header\\\":\\\"Edit component properties\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/elements_branch.png\\\",\\\"disabledIcon\\\":\\\"/temp/elements_branch_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the properties for the component.\\\",\\\"type\\\":\\\"common\\\"},{\\\"click\\\":\\\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={9157CC8C-5760-5114-9F2D-93CBE39B30DC},renderingId={EF310C84-8E26-5532-9A11-DB68DCF37BEF},id={27F322AF-3D30-5F2F-ADA2-DAB630D35EC6})',null,false)\\\",\\\"header\\\":\\\"dsHeaderParameter\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/data.png\\\",\\\"disabledIcon\\\":\\\"/temp/data_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"dsTooltipParameter\\\",\\\"type\\\":\\\"datasourcesmenu\\\"},{\\\"click\\\":\\\"chrome:rendering:personalize({command:\\\\\\\"webedit:personalize\\\\\\\"})\\\",\\\"header\\\":\\\"Personalize\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/users_family.png\\\",\\\"disabledIcon\\\":\\\"/temp/users_family_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Create or edit personalization for this component.\\\",\\\"type\\\":\\\"sticky\\\"},{\\\"click\\\":\\\"chrome:common:edititem({command:\\\\\\\"webedit:open\\\\\\\"})\\\",\\\"header\\\":\\\"Edit the related item\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/cubes.png\\\",\\\"disabledIcon\\\":\\\"/temp/cubes_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the related item in the Content Editor.\\\",\\\"type\\\":\\\"datasourcesmenu\\\"},{\\\"click\\\":\\\"chrome:rendering:delete\\\",\\\"header\\\":\\\"Delete\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/delete.png\\\",\\\"disabledIcon\\\":\\\"/temp/delete_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Remove component.\\\",\\\"type\\\":\\\"sticky\\\"}],\\\"contextItemUri\\\":\\\"sitecore://master/{27F322AF-3D30-5F2F-ADA2-DAB630D35EC6}?lang=en&ver=1\\\",\\\"custom\\\":{\\\"renderingID\\\":\\\"EF310C848E2655329A11DB68DCF37BEF\\\",\\\"editable\\\":\\\"true\\\"},\\\"displayName\\\":\\\"Content Block\\\",\\\"expandedDisplayName\\\":null}\",\"attributes\":{\"type\":\"text/sitecore\",\"chrometype\":\"rendering\",\"kind\":\"open\",\"hintname\":\"Content Block\",\"id\":\"r_9157CC8C576051149F2D93CBE39B30DC\",\"class\":\"scpm\",\"data-selectable\":\"true\"}},{\"uid\":\"9157cc8c-5760-5114-9f2d-93cbe39b30dc\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{27F322AF-3D30-5F2F-ADA2-DAB630D35EC6}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"GraphQL Sample 1\",\"editable\":\"{\\\"contextItem\\\":{\\\"id\\\":\\\"27f322af-3d30-5f2f-ada2-dab630d35ec6\\\",\\\"version\\\":1,\\\"language\\\":\\\"en\\\",\\\"revision\\\":\\\"ad0b2a91d2c5484facf59feb6b49dff1\\\"},\\\"fieldId\\\":\\\"fda8c2d4-7536-5b22-a75e-2a8ed09c6302\\\",\\\"fieldType\\\":\\\"Single-Line Text\\\",\\\"fieldWebEditParameters\\\":{\\\"prevent-line-break\\\":\\\"true\\\"},\\\"commands\\\":[{\\\"click\\\":\\\"chrome:common:edititem({command:\\\\\\\"webedit:open\\\\\\\"})\\\",\\\"header\\\":\\\"Edit the related item\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/cubes.png\\\",\\\"disabledIcon\\\":\\\"/temp/cubes_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the related item in the Content Editor.\\\",\\\"type\\\":\\\"common\\\"},{\\\"click\\\":\\\"chrome:rendering:personalize({command:\\\\\\\"webedit:personalize\\\\\\\"})\\\",\\\"header\\\":\\\"Personalize\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/users_family.png\\\",\\\"disabledIcon\\\":\\\"/temp/users_family_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Create or edit personalization for this component.\\\",\\\"type\\\":\\\"sticky\\\"}],\\\"contextItemUri\\\":\\\"sitecore://master/{27F322AF-3D30-5F2F-ADA2-DAB630D35EC6}?lang=en&ver=1\\\",\\\"custom\\\":{},\\\"displayName\\\":\\\"heading\\\",\\\"expandedDisplayName\\\":null}GraphQL Sample 1\"},\"content\":{\"value\":\"

A child route here to illustrate the power of GraphQL queries. Back to GraphQL route

\\n\",\"editable\":\"{\\\"contextItem\\\":{\\\"id\\\":\\\"27f322af-3d30-5f2f-ada2-dab630d35ec6\\\",\\\"version\\\":1,\\\"language\\\":\\\"en\\\",\\\"revision\\\":\\\"ad0b2a91d2c5484facf59feb6b49dff1\\\"},\\\"fieldId\\\":\\\"a40366ce-32d2-5868-a373-a20ec9c51573\\\",\\\"fieldType\\\":\\\"Rich Text\\\",\\\"fieldWebEditParameters\\\":{},\\\"commands\\\":[{\\\"click\\\":\\\"chrome:field:editcontrol({command:\\\\\\\"webedit:edithtml\\\\\\\"})\\\",\\\"header\\\":\\\"Edit Text\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/pencil.png\\\",\\\"disabledIcon\\\":\\\"/temp/pencil_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the text\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:execute({command:\\\\\\\"bold\\\\\\\", userInterface:true, value:true})\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/font_style_bold.png\\\",\\\"disabledIcon\\\":\\\"/temp/font_style_bold_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Bold\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:execute({command:\\\\\\\"Italic\\\\\\\", userInterface:true, value:true})\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/font_style_italics.png\\\",\\\"disabledIcon\\\":\\\"/temp/font_style_italics_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Italic\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:execute({command:\\\\\\\"Underline\\\\\\\", userInterface:true, value:true})\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/font_style_underline.png\\\",\\\"disabledIcon\\\":\\\"/temp/font_style_underline_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Underline\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:insertexternallink\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/earth_link.png\\\",\\\"disabledIcon\\\":\\\"/temp/earth_link_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Insert an external link into the text field.\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:insertlink\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/link.png\\\",\\\"disabledIcon\\\":\\\"/temp/link_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Insert a link into the text field.\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:removelink\\\",\\\"header\\\":\\\"\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/link_broken.png\\\",\\\"disabledIcon\\\":\\\"/temp/link_broken_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Remove link.\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:field:insertimage\\\",\\\"header\\\":\\\"Insert image\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/photo_landscape.png\\\",\\\"disabledIcon\\\":\\\"/temp/photo_landscape_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Insert an image into the text field.\\\",\\\"type\\\":null},{\\\"click\\\":\\\"chrome:common:edititem({command:\\\\\\\"webedit:open\\\\\\\"})\\\",\\\"header\\\":\\\"Edit the related item\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/cubes.png\\\",\\\"disabledIcon\\\":\\\"/temp/cubes_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Edit the related item in the Content Editor.\\\",\\\"type\\\":\\\"common\\\"},{\\\"click\\\":\\\"chrome:rendering:personalize({command:\\\\\\\"webedit:personalize\\\\\\\"})\\\",\\\"header\\\":\\\"Personalize\\\",\\\"icon\\\":\\\"/temp/iconcache/office/16x16/users_family.png\\\",\\\"disabledIcon\\\":\\\"/temp/users_family_disabled16x16.png\\\",\\\"isDivider\\\":false,\\\"tooltip\\\":\\\"Create or edit personalization for this component.\\\",\\\"type\\\":\\\"sticky\\\"}],\\\"contextItemUri\\\":\\\"sitecore://master/{27F322AF-3D30-5F2F-ADA2-DAB630D35EC6}?lang=en&ver=1\\\",\\\"custom\\\":{},\\\"displayName\\\":\\\"content\\\",\\\"expandedDisplayName\\\":null}

A child route here to illustrate the power of GraphQL queries. Back to GraphQL route

\\n
\"}}},{\"name\":\"code\",\"type\":\"text/sitecore\",\"contents\":\"\",\"attributes\":{\"type\":\"text/sitecore\",\"id\":\"scEnclosingTag_r_\",\"chrometype\":\"rendering\",\"kind\":\"close\",\"hintkey\":\"Content Block\",\"class\":\"scpm\"}},{\"name\":\"code\",\"type\":\"text/sitecore\",\"contents\":\"\",\"attributes\":{\"type\":\"text/sitecore\",\"id\":\"scEnclosingTag_\",\"chrometype\":\"placeholder\",\"kind\":\"close\",\"hintname\":\"Main\",\"class\":\"scpm\"}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://sc100xm1cm:443/?sc_itemid={cfdd7ba2-e646-5294-87fc-6fad34451a97}&sc_ee_fb=false&sc_lang=en\",\"path\":\"/\",\"querystring\":{\"sc_itemid\":\"{cfdd7ba2-e646-5294-87fc-6fad34451a97}\",\"sc_ee_fb\":\"false\",\"sc_lang\":\"en\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.45 Safari/537.36 Edg/84.0.522.20\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + private const string PreviewRequest = + """{"id":"jss-sample-app","args":["/?sc_itemid=%7bcfdd7ba2-e646-5294-87fc-6fad34451a97%7d&sc_ee_fb=false&sc_lang=en&sc_mode=preview&sc_debug=0&sc_trace=0&sc_prof=0&sc_ri=0&sc_rb=0","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jss-sample-app\"},\"pageState\":\"preview\",\"language\":\"en\",\"itemPath\":\"/graphql/sample-1\"},\"route\":{\"name\":\"sample-1\",\"displayName\":\"sample-1\",\"fields\":{\"pageTitle\":{\"value\":\"Sample 1 Page Title\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"cfdd7ba2-e646-5294-87fc-6fad34451a97\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"5179e218-3df6-5af7-8147-d2d4c05da992\",\"templateId\":\"dfe73d70-9835-584e-b0f5-28c58ab064d7\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"9157cc8c-5760-5114-9f2d-93cbe39b30dc\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{27F322AF-3D30-5F2F-ADA2-DAB630D35EC6}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"GraphQL Sample 1\"},\"content\":{\"value\":\"

A child route here to illustrate the power of GraphQL queries. Back to GraphQL route

\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://sc100xm1cm:443/?sc_itemid={cfdd7ba2-e646-5294-87fc-6fad34451a97}&sc_ee_fb=false&sc_lang=en&sc_mode=preview&sc_debug=0&sc_trace=0&sc_prof=0&sc_ri=0&sc_rb=0\",\"path\":\"/\",\"querystring\":{\"sc_itemid\":\"{cfdd7ba2-e646-5294-87fc-6fad34451a97}\",\"sc_ee_fb\":\"false\",\"sc_lang\":\"en\",\"sc_mode\":\"preview\",\"sc_debug\":\"0\",\"sc_trace\":\"0\",\"sc_prof\":\"0\",\"sc_ri\":\"0\",\"sc_rb\":\"0\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.45 Safari/537.36 Edg/84.0.522.20\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + private const string JssEditingSecret = "mysecret"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Behaviors.Add(new OmitOnRecursionBehavior()); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_Guarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_Guarded(ExperienceEditorMiddleware sut) + { + Func act = + () => sut.Invoke(null!); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_NextMiddlewareCalled([Frozen] RequestDelegate next, HttpContext httpContext, ExperienceEditorMiddleware sut) + { + await sut.Invoke(httpContext); + + Received.InOrder(() => next.Invoke(httpContext)); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_WhenRequestFromIncorrectEndpoint_DoesNotProcess(IOptions options, HttpContext httpContext, ExperienceEditorMiddleware sut) + { + httpContext.Request.Method = HttpMethods.Post; + httpContext.Request.Path = new PathString("/jss-render"); + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(EeSampleRequest)); + FeatureCollection fc = new(); + httpContext.Features.Returns(fc); + + options.Value.Endpoint = "/incorrect-endpoint"; + + await sut.Invoke(httpContext); + + httpContext.GetSitecoreRenderingContext().Should().BeNull("EE Middleware not run because request has incorrect endpoint"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_WhenGetRequest_DoesNotProcess(HttpContext httpContext, ExperienceEditorMiddleware sut) + { + FeatureCollection fc = new(); + httpContext.Features.Returns(fc); + + await sut.Invoke(httpContext); + + httpContext.GetSitecoreRenderingContext().Should().BeNull("EE Middleware not run because request is a GET"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_WhenCorrectRequest_Processes([Frozen] IOptions options, HttpContext httpContext, ExperienceEditorMiddleware sut) + { + options.Value.Endpoint = "/jss-render"; + options.Value.JssEditingSecret = JssEditingSecret; + FeatureCollection fc = new(); + + httpContext.Request.Method = HttpMethods.Post; + httpContext.Request.Path = new PathString("/jss-render"); + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(EeSampleRequest)); + httpContext.Response.Body = new MemoryStream(); + httpContext.Features.Returns(fc); + + await sut.Invoke(httpContext); + + httpContext.GetSitecoreRenderingContext().Should().NotBeNull(); + } + + [Theory] + [InlineAutoNSubstituteData(EditRequest)] + [InlineAutoNSubstituteData(PreviewRequest)] + public async Task Invoke_SetsCorrectRequestPath(string request, ILogger logger, HttpContext httpContext) + { + string path = null!; + + // ReSharper disable once StringLiteralTypo + IOptions options = new OptionsWrapper(new ExperienceEditorOptions { JssEditingSecret = "mysecret" }); + ExperienceEditorMiddleware sut = new( + context => + { + path = context.Request.Path; + return Task.CompletedTask; + }, + options, + new JsonLayoutServiceSerializer(), + logger); + + options.Value.Endpoint = "/jss-render"; + httpContext.Request.Method = HttpMethods.Post; + httpContext.Request.Path = new PathString("/jss-render"); + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(request)); + httpContext.Response.Body = new MemoryStream(); + + await sut.Invoke(httpContext); + + path.Should().Be("/graphql/sample-1"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Models/ExperienceEditorPostModelFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Models/ExperienceEditorPostModelFixture.cs new file mode 100644 index 0000000..9341313 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Models/ExperienceEditorPostModelFixture.cs @@ -0,0 +1,50 @@ +using AutoFixture.Idioms; +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.Models; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.Models; + +public class ExperienceEditorPostModelFixture +{ + private const string RequestExample = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle"}"""; + + [Theory] + [AutoNSubstituteData] + public void Ctor_IsGuarded(GuardClauseAssertion guard) + { + // Act / Assert + guard.VerifyConstructors(); + } + + [Fact] + public void Ctor_SetsDefaults() + { + // Arrange / Act + ExperienceEditorPostModel sut = new(); + + // Assert + sut.Id.Should().BeNull(); + sut.FunctionName.Should().BeNull(); + sut.ModuleName.Should().BeNull(); + sut.Args.Should().BeEmpty("Assigned by default to an empty list"); + } + + [Fact] + public void EeRequestCanBeDeserializedToAnInstance() + { + ExperienceEditorPostModel? model = Serializer.Deserialize(RequestExample); + + JsonLayoutServiceSerializer serializer = new(); + SitecoreLayoutResponseContent? layout = serializer.Deserialize(model!.Args[1]); + + model.Should().NotBeNull(); + layout.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.csproj new file mode 100644 index 0000000..0452531 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataBuilderFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataBuilderFixture.cs new file mode 100644 index 0000000..4fadc22 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataBuilderFixture.cs @@ -0,0 +1,138 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.TagHelpers; + +public class ChromeDataBuilderFixture +{ + private readonly IChromeDataBuilder _chromeDataBuilder = new ChromeDataBuilder(); + + [Fact] + public void MapButtonToCommand_DividerEditButton_ReturnsDividerCommand() + { + // Arrange + DividerEditButton button = new(); + + // Act + ChromeCommand command = _chromeDataBuilder.MapButtonToCommand(button, null, null); + + // Assert + command.Click.Should().Be("chrome:dummy"); + command.Icon.Should().Be(button.Icon); + command.IsDivider.Should().BeTrue(); + command.Header.Should().Be(button.Header); + command.Tooltip.Should().BeNull(); + command.Type.Should().Be("separator"); + } + + [Fact] + public void MapButtonToCommand_WebEditButtonWithClick_ReturnsChromeCommand() + { + // Arrange + WebEditButton button = new() + { + Header = "WebEditButton", + Icon = "/~/icon/Office/16x16/document_selection.png", + Click = "javascript:alert(\"An edit frame button was just clicked!\")", + Tooltip = "Doesn't do much, just a web edit button example" + }; + + // Act + ChromeCommand command = _chromeDataBuilder.MapButtonToCommand(button, null, null); + + // Assert + command.Click.Should().Be(button.Click); + command.Icon.Should().Be(button.Icon); + command.IsDivider.Should().BeFalse(); + command.Header.Should().Be(button.Header); + command.Tooltip.Should().Be(button.Tooltip); + command.Type.Should().Be(button.Type); + } + + [Fact] + public void MapButtonToCommand_FieldEditButtonWithFieldsAndItemId_ReturnsChromeCommand() + { + // Arrange + FieldEditButton button = new() + { + Header = "FieldEditButton", + Icon = "/~/icon/Office/16x16/pencil.png", + Fields = ["applyRedToText", "sampleList"], + Tooltip = "Allows you to open field editor for specified fields" + }; + string itemId = Guid.NewGuid().ToString(); + + // Act + ChromeCommand command = _chromeDataBuilder.MapButtonToCommand(button, itemId, null); + + // Assert + command.Click.Should().Be($"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={DefaultEditFrameButtonIds.Edit}, fields={string.Join('|', button.Fields)}, id={itemId})',null,false)"); + command.Icon.Should().Be(button.Icon); + command.IsDivider.Should().BeFalse(); + command.Header.Should().Be(button.Header); + command.Tooltip.Should().Be(button.Tooltip); + command.Type.Should().BeNull(); + } + + [Fact] + public void MapButtonToCommand_WebEditButtonWithInvalidClick_ReturnsChromeCommand() + { + // Arrange + WebEditButton button = new() + { + Header = "WebEditButton", + Icon = "/~/icon/Office/16x16/document_selection.png", + Click = "test:alert(\"Missing parentheses!\"", + Tooltip = "Doesn't do much, just a web edit button example" + }; + string itemId = Guid.NewGuid().ToString(); + + // Act + Action action = () => _chromeDataBuilder.MapButtonToCommand(button, itemId, null); + + // Assert + action.Should().Throw("Message with arguments must end with )."); + } + + [Fact] + public void MapButtonToCommand_WebEditButtonWithParameters_ReturnsChromeCommand() + { + // Arrange + WebEditButton button = new() + { + Header = "WebEditButton", + Icon = "/~/icon/Office/16x16/document_selection.png", + Click = "test:MethodWithParams(\"\")", + Tooltip = "Doesn't do much, just a web edit button example", + Parameters = new Dictionary + { + { "btnParam1", "btnVal1" }, + { "btnParam2", "btnVal2" }, + } + }; + string itemId = Guid.NewGuid().ToString(); + Dictionary frameParams = new() + { + { + "frameParam1", "frameVal1" + }, + { + "frameParam2", "frameVal2" + }, + }; + + // Act + ChromeCommand command = _chromeDataBuilder.MapButtonToCommand(button, itemId, frameParams); + + // Assert + command.Click.Should().Be($"javascript:Sitecore.PageModes.PageEditor.postRequest('test:MethodWithParams(\"\"=, id={itemId}, btnParam1=btnVal1, btnParam2=btnVal2, frameParam1=frameVal1, frameParam2=frameVal2)',null,false)"); + command.Icon.Should().Be(button.Icon); + command.IsDivider.Should().BeFalse(); + command.Header.Should().Be(button.Header); + command.Tooltip.Should().Be(button.Tooltip); + command.Type.Should().Be(button.Type); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataSerializerFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataSerializerFixture.cs new file mode 100644 index 0000000..82aea7a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/ChromeDataSerializerFixture.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.TagHelpers; + +public class ChromeDataSerializerFixture +{ + private readonly ChromeDataSerializer _serializer = new(); + + [Fact] + public void Serialize_EmptyData_ShouldReturnEmptyJsonObject() + { + // Arrange + Dictionary data = []; + + // Act + string result = _serializer.Serialize(data); + + // Assert + result.Should().Be("{}"); + } + + [Fact] + public void Serialize_WithValidData_ShouldReturnJson() + { + // Arrange + Dictionary data = new() + { + ["class"] = "my-super-class", + ["displayName"] = "my-super-name", + ["itsNull"] = null + }; + + // Act + string result = _serializer.Serialize(data); + + // Assert + result.Should().Be("{\"class\":\"my-super-class\",\"displayName\":\"my-super-name\",\"itsNull\":null}"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/EditFrameTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/EditFrameTagHelperFixture.cs new file mode 100644 index 0000000..3d114f5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.ExperienceEditor.Tests/TagHelpers/EditFrameTagHelperFixture.cs @@ -0,0 +1,261 @@ +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers; +using Sitecore.AspNetCore.SDK.ExperienceEditor.TagHelpers.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.ExperienceEditor.Tests.TagHelpers; + +public class EditFrameTagHelperFixture +{ + private const string OriginalContent = "Content for Edit Frame"; + + private static IChromeDataBuilder _chromeDataBuilder = null!; + + private static IChromeDataSerializer _chromeDataSerializer = null!; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + _chromeDataBuilder = f.Freeze(); + _chromeDataSerializer = f.Freeze(); + + // Configure the View Context - required as property on PlaceholderTagHelper + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + FeatureCollection features = new(); + + viewContext.HttpContext.Features.Returns(features); + f.Inject(viewContext); + + // Configure the TagHelperOutput + TagHelperOutput tagHelperOutput = new("div", [], (result, encoder) => + { + DefaultTagHelperContent tagHelperContent = new(); + _ = tagHelperContent.SetHtmlContent(OriginalContent); + return Task.FromResult(tagHelperContent); + }); + + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PageInNormalMode_OutputContentWithoutChanges( + EditFrameTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = false + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().BeEmpty(); + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViewContextIsNull_ThrowsNullReferenceException( + EditFrameTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + sut.ViewContext = null; + Func act = + () => sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("ViewContext parameter cannot be null."); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PageInEditModeAndWithoutAttributes_OutputContainsEditFrameMarkup( + EditFrameTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + const string chromeDataPart = "Hello, it is Chrome Data"; + _chromeDataSerializer.Serialize(Arg.Any>()).Returns(chromeDataPart); + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + sut.Source = null; + sut.Tooltip = null; + sut.Parameters = null; + sut.Buttons = null; + sut.CssClass = null; + sut.Title = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($"
{chromeDataPart}{OriginalContent}
"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PageInEditModeAndWithSourceAttribute_OutputContainsEditFrameMarkup( + EditFrameTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + EditFrameDataSource source = new() + { + DatabaseName = "master", + Language = "en", + ItemId = Guid.NewGuid().ToString(), + }; + const string chromeDataPart = "Hello, it is Chrome Data"; + _chromeDataSerializer.Serialize(Arg.Any>()).Returns(chromeDataPart); + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + sut.Source = source; + sut.Tooltip = null; + sut.Parameters = null; + sut.Buttons = null; + sut.CssClass = null; + sut.Title = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($"
{chromeDataPart}{OriginalContent}
"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PageInEditModeAndWithButtonsAttribute_CommandBuilderShouldBeCalled( + EditFrameTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + List buttons = + [ + new WebEditButton(), + new DividerEditButton() + ]; + const string chromeDataPart = "Hello, it is Chrome Data"; + _chromeDataSerializer.Serialize(Arg.Any>()).Returns(chromeDataPart); + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + sut.Source = null; + sut.Tooltip = null; + sut.Parameters = null; + sut.Buttons = buttons; + sut.CssClass = null; + sut.Title = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + foreach (EditButtonBase button in buttons) + { + _chromeDataBuilder.Received(1).MapButtonToCommand(button, null, null); + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs new file mode 100644 index 0000000..b5bc1fb --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs @@ -0,0 +1,74 @@ +using AutoFixture.Xunit2; +using FluentAssertions; +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.GraphQL.Exceptions; +using Sitecore.AspNetCore.SDK.GraphQL.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.GraphQL.Tests.Extensions; + +public class GraphQlConfigurationExtensionsFixture +{ + [Theory] + [AutoNSubstituteData] + public void AddGraphQlClient_NullProperties_ThrowsExceptions(IServiceCollection serviceCollection) + { + Func servicesNull = + () => GraphQlConfigurationExtensions.AddGraphQlClient(null!, null!); + Func configNull = + () => serviceCollection.AddGraphQlClient(null!); + + servicesNull.Should().Throw().WithParameterName("services"); + configNull.Should().Throw().WithParameterName("configuration"); + } + + [Fact] + public void AddGraphQlClient_EmptyApiKey_InConfiguration_ThrowsExceptions() + { + Func act = + () => Substitute.For().AddGraphQlClient(delegate { }); + act.Should().Throw() + .WithMessage("Empty ApiKey, provided in GraphQLClientOptions."); + } + + [Theory] + [AutoData] + public void AddGraphQlClient_EmptyEndpointUri_InConfiguration_ThrowsExceptions(string apiKey) + { + Func act = + () => Substitute.For().AddGraphQlClient(configuration => + { + configuration.ApiKey = apiKey; + }); + act.Should().Throw() + .WithMessage("Empty EndPoint, provided in GraphQLClientOptions."); + } + + [Theory] + [AutoData] + public void AddGraphQlClient_AddConfiguredGraphQlClient_To_ServiceCollection(string apiKey, Uri endpointUri, string defaultSiteName) + { + // Arrange + ServiceCollection serviceCollection = []; + + // Act + serviceCollection.AddGraphQlClient( + configuration => + { + configuration.ApiKey = apiKey; + configuration.EndPoint = endpointUri; + configuration.DefaultSiteName = defaultSiteName; + }); + GraphQLHttpClient? graphQlClient = serviceCollection.BuildServiceProvider().GetService() as GraphQLHttpClient; + + // Assert + graphQlClient.Should().NotBeNull(); + graphQlClient!.Options.EndPoint.Should().Be(endpointUri); + graphQlClient.HttpClient.DefaultRequestHeaders.Contains("sc_apikey").Should().BeTrue(); + apiKey.Should().Be(graphQlClient.HttpClient.DefaultRequestHeaders.GetValues("sc_apikey").FirstOrDefault()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Sitecore.AspNetCore.SDK.GraphQL.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Sitecore.AspNetCore.SDK.GraphQL.Tests.csproj new file mode 100644 index 0000000..5aa340f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Sitecore.AspNetCore.SDK.GraphQL.Tests.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/ContextFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/ContextFixture.cs new file mode 100644 index 0000000..56c204f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/ContextFixture.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests; + +public class ContextFixture +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Can't be done, confuses the compiler types.")] + public static TheoryData Serializers => new() + { + new JsonLayoutServiceSerializer() + }; + + [Theory] + [MemberData(nameof(Serializers))] + public void Context_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + Context? resultContext = result?.Sitecore?.Context; + + dynamic? expectedContext = jsonModel.sitecore.context; + + resultContext.Should().NotBeNull(); + resultContext!.IsEditing.Should().Be((bool)expectedContext.pageEditing); + + resultContext.Site.Should().NotBeNull(); + resultContext.Site!.Name.Should().Be((string)expectedContext.site.name); + resultContext.PageState.Should().Be((PageState)expectedContext.pageState); + resultContext.Language.Should().Be((string)expectedContext.language); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/DeviceFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/DeviceFixture.cs new file mode 100644 index 0000000..2f68b2e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/DeviceFixture.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests; + +public class DeviceFixture +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Can't be done, confuses the compiler types.")] + public static TheoryData Serializers => new() + { + new JsonLayoutServiceSerializer() + }; + + [Theory] + [MemberData(nameof(Serializers))] + public void Device_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + Device resultDevice = result!.Sitecore!.Devices[0]; + + dynamic? expectedDevice = jsonModel.sitecore.devices[0]; + + resultDevice.Should().NotBeNull(); + resultDevice.Id.Should().Be((string)expectedDevice.id); + resultDevice.LayoutId.Should().Be((string)expectedDevice.layoutId); + resultDevice.Placeholders.Should().BeEmpty(); + resultDevice.Renderings.Should().HaveCount(3); + + for (int i = 0; i < 3; i++) + { + resultDevice.Renderings[i].Id.Should().Be((string)expectedDevice.renderings[i].id); + resultDevice.Renderings[i].InstanceId.Should().Be((string)expectedDevice.renderings[i].instanceId); + resultDevice.Renderings[i].PlaceholderKey.Should().Be((string)expectedDevice.renderings[i].placeholderKey); + resultDevice.Renderings[i].Parameters.Should().BeEmpty(); + resultDevice.Renderings[i].Caching!.Cacheable.Should().Be(null); + resultDevice.Renderings[i].Caching!.ClearOnIndexUpdate.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByData.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByDevice.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByLogin.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByParameters.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByQueryString.Should().Be(null); + resultDevice.Renderings[i].Caching!.VaryByUser.Should().Be(null); + resultDevice.Renderings[i].Personalization!.Conditions.Should().Be(null); + resultDevice.Renderings[i].Personalization!.MultiVariateTestId.Should().Be(null); + resultDevice.Renderings[i].Personalization!.PersonalizationTest.Should().Be(null); + resultDevice.Renderings[i].Personalization!.Rules.Should().Be(null); + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/EditableChromeFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/EditableChromeFixture.cs new file mode 100644 index 0000000..dbdb6e0 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/EditableChromeFixture.cs @@ -0,0 +1,142 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests; + +public class EditableChromeFixture +{ + public static TheoryData Data => new() + { + { + new JsonLayoutServiceSerializer(), + "edit-in-horizon-mode" + }, + { + new JsonLayoutServiceSerializer(), + "edit" + } + }; + + [Theory] + [MemberData(nameof(Data))] + public void Component_OpeningChrome_CanBeRead(ISitecoreLayoutSerializer serializer, string editResponseFileName) + { + // Arrange + string json = File.ReadAllText($"./Json/{editResponseFileName}.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + EditableChrome? resultChrome = result?.Sitecore?.Route?.Placeholders["jss-main"].ChromeAt(1); + + dynamic? expectedChrome = jsonModel.sitecore.route.placeholders["jss-main"][1]; + + resultChrome.Should().NotBeNull(); + resultChrome!.Name.Should().Be((string)expectedChrome.name); + resultChrome.Type.Should().Be((string)expectedChrome.type); + resultChrome.Content.Should().Be((string)expectedChrome.contents); + resultChrome.Attributes.Should().HaveCount(7); + resultChrome.Attributes["type"].Should().Be((string)expectedChrome.attributes.type); + resultChrome.Attributes["chrometype"].Should().Be((string)expectedChrome.attributes.chrometype); + resultChrome.Attributes["kind"].Should().Be((string)expectedChrome.attributes.kind); + resultChrome.Attributes["id"].Should().Be((string)expectedChrome.attributes.id); + resultChrome.Attributes["hintname"].Should().Be((string)expectedChrome.attributes.hintname); + resultChrome.Attributes["class"].Should().Be((string)expectedChrome.attributes.@class); + resultChrome.Attributes["data-selectable"].Should().Be((string)expectedChrome.attributes["data-selectable"]); + } + + [Theory] + [MemberData(nameof(Data))] + public void Component_ClosingChrome_CanBeRead(ISitecoreLayoutSerializer serializer, string editResponseFileName) + { + // Arrange + string json = File.ReadAllText($"./Json/{editResponseFileName}.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + EditableChrome? resultChrome = result?.Sitecore?.Route?.Placeholders["jss-main"].ChromeAt(3); + + dynamic? expectedChrome = jsonModel.sitecore.route.placeholders["jss-main"][3]; + + resultChrome.Should().NotBeNull(); + resultChrome!.Name.Should().Be((string)expectedChrome.name); + resultChrome.Type.Should().Be((string)expectedChrome.type); + resultChrome.Content.Should().Be((string)expectedChrome.contents); + resultChrome.Attributes.Should().HaveCount(6); + resultChrome.Attributes["type"].Should().Be((string)expectedChrome.attributes.type); + resultChrome.Attributes["chrometype"].Should().Be((string)expectedChrome.attributes.chrometype); + resultChrome.Attributes["kind"].Should().Be((string)expectedChrome.attributes.kind); + resultChrome.Attributes["id"].Should().Be((string)expectedChrome.attributes.id); + resultChrome.Attributes["hintkey"].Should().Be((string)expectedChrome.attributes.hintkey); + resultChrome.Attributes["class"].Should().Be((string)expectedChrome.attributes.@class); + } + + [Theory] + [MemberData(nameof(Data))] + public void Placeholder_OpeningChrome_CanBeRead(ISitecoreLayoutSerializer serializer, string editResponseFileName) + { + // Arrange + string json = File.ReadAllText($"./Json/{editResponseFileName}.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + EditableChrome? resultChrome = result?.Sitecore?.Route?.Placeholders["jss-main"].ChromeAt(0); + + dynamic? expectedChrome = jsonModel.sitecore.route.placeholders["jss-main"][0]; + + resultChrome.Should().NotBeNull(); + resultChrome!.Name.Should().Be((string)expectedChrome.name); + resultChrome.Type.Should().Be((string)expectedChrome.type); + resultChrome.Content.Should().Be((string)expectedChrome.contents); + resultChrome.Attributes.Should().HaveCount(7); + resultChrome.Attributes["type"].Should().Be((string)expectedChrome.attributes.type); + resultChrome.Attributes["chrometype"].Should().Be((string)expectedChrome.attributes.chrometype); + resultChrome.Attributes["kind"].Should().Be((string)expectedChrome.attributes.kind); + resultChrome.Attributes["id"].Should().Be((string)expectedChrome.attributes.id); + resultChrome.Attributes["key"].Should().Be((string)expectedChrome.attributes.key); + resultChrome.Attributes["class"].Should().Be((string)expectedChrome.attributes.@class); + resultChrome.Attributes["data-selectable"].Should().Be((string)expectedChrome.attributes["data-selectable"]); + } + + [Theory] + [MemberData(nameof(Data))] + public void Placeholder_ClosingChrome_CanBeRead(ISitecoreLayoutSerializer serializer, string editResponseFileName) + { + // Arrange + string json = File.ReadAllText($"./Json/{editResponseFileName}.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + EditableChrome? resultChrome = result?.Sitecore?.Route?.Placeholders["jss-main"].ChromeAt(7); + + dynamic? expectedChrome = jsonModel.sitecore.route.placeholders["jss-main"][7]; + + resultChrome.Should().NotBeNull(); + resultChrome!.Name.Should().Be((string)expectedChrome.name); + resultChrome.Type.Should().Be((string)expectedChrome.type); + resultChrome.Content.Should().Be((string)expectedChrome.contents); + resultChrome.Attributes.Should().HaveCount(6); + resultChrome.Attributes["type"].Should().Be((string)expectedChrome.attributes.type); + resultChrome.Attributes["chrometype"].Should().Be((string)expectedChrome.attributes.chrometype); + resultChrome.Attributes["kind"].Should().Be((string)expectedChrome.attributes.kind); + resultChrome.Attributes["id"].Should().Be((string)expectedChrome.attributes.id); + resultChrome.Attributes["hintname"].Should().Be((string)expectedChrome.attributes.hintname); + resultChrome.Attributes["class"].Should().Be((string)expectedChrome.attributes.@class); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs new file mode 100644 index 0000000..9b2f229 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/FieldsFixture.cs @@ -0,0 +1,496 @@ +using System.Globalization; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.MockModels; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests; + +public class FieldsFixture +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Can't be done, confuses the compiler types.")] + public static TheoryData Serializers => new() + { + new JsonLayoutServiceSerializer() + }; + + [Theory] + [MemberData(nameof(Serializers))] + public void Route_TextField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + TextField? routePageTitle = result?.Sitecore?.Route?.Fields["pageTitle"].Read(); + routePageTitle!.Value.Should().Be(jsonModel.sitecore.route.fields.pageTitle.value.Value); + routePageTitle.EditableMarkup.Should().Be(jsonModel.sitecore.route.fields.pageTitle.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_TextField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + Component? component = result?.Sitecore?.Route?.Placeholders["jss-main"].ComponentAt(2); + TextField? field = component?.Fields["heading"].Read(); + field!.Value.Should().Be(jsonModel.sitecore.route.placeholders["jss-main"][2].fields.heading.value.Value); + field.EditableMarkup.Should().Be(jsonModel.sitecore.route.placeholders["jss-main"][2].fields.heading.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_HyperLinkField_UsingExternalLink_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + HyperLinkField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(26)? + .Fields["paramsLink"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][26] + .fields.paramsLink; + + resultField!.Value.Should().NotBeNull(); + resultField.Value.Class.Should().Be((string)expectedField.value.@class); + resultField.Value.Target.Should().Be((string)expectedField.value.target); + resultField.Value.Title.Should().Be((string)expectedField.value.title); + resultField.Value.Href.Should().Be((string)expectedField.value.url); + resultField.Value.Href.Should().Be((string)expectedField.value.href); + resultField.Value.Text.Should().Be((string)expectedField.value.text); + + resultField.EditableMarkupFirst.Should().Be(expectedField.editableFirstPart.Value); + resultField.EditableMarkupLast.Should().Be(expectedField.editableLastPart.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_HyperLinkField_UsingInternalLink_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + HyperLinkField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(26)? + .Fields["internalLink"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][26] + .fields.internalLink; + + resultField!.Value.Should().NotBeNull(); + resultField.Value.Class.Should().BeNull(); + resultField.Value.Target.Should().BeNull(); + resultField.Value.Title.Should().BeNull(); + resultField.Value.Href.Should().Be((string)expectedField.value.href); + resultField.Value.Text.Should().BeNull(); + + resultField.EditableMarkupFirst.Should().Be(expectedField.editableFirstPart.Value); + resultField.EditableMarkupLast.Should().Be(expectedField.editableLastPart.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_HyperLinkField_UsingEmailLink_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + HyperLinkField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(26)? + .Fields["emailLink"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][26] + .fields.emailLink; + + resultField!.Value.Should().NotBeNull(); + resultField.Value.Class.Should().BeNull(); + resultField.Value.Target.Should().BeNull(); + resultField.Value.Title.Should().BeNull(); + resultField.Value.Href.Should().Be((string)expectedField.value.href); + resultField.Value.Text.Should().Be((string)expectedField.value.text); + + resultField.EditableMarkupFirst.Should().Be(expectedField.editableFirstPart.Value); + resultField.EditableMarkupLast.Should().Be(expectedField.editableLastPart.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_ItemLinkField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + ItemLinkField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(29)? + .Fields["sharedItemLink"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][29] + .fields.sharedItemLink; + + resultField!.Id.Should().Be(expectedField.id.Value); + resultField.Url.Should().Be(expectedField.url.Value); + + TextField? textField = resultField.Fields["textField"].Read(); + textField!.Value.Should().Be(expectedField.fields.textField.value.Value); + textField.EditableMarkup.Should().Be(expectedField.fields.textField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_StrongTypeItemLinkField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + ItemLinkField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(29)? + .Fields["sharedItemLink"] + .Read>(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][29] + .fields.sharedItemLink; + + resultField!.Id.Should().Be(expectedField.id.Value); + resultField.Url.Should().Be(expectedField.url.Value); + + TextField textField = resultField.Target!.TextField; + textField.Value.Should().Be(expectedField.fields.textField.value.Value); + textField.EditableMarkup.Should().Be(expectedField.fields.textField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_ContentListField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + ContentListField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(32)? + .Fields["sharedContentList"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][32] + .fields.sharedContentList; + + resultField!.Should().HaveCount(2); + resultField![0].Id.Should().Be(expectedField[0].id.Value); + + TextField? firstField = resultField[0].Fields["textField"].Read(); + firstField!.Value.Should().Be(expectedField[0].fields.textField.value.Value); + firstField.EditableMarkup.Should().Be(expectedField[0].fields.textField.editable.Value); + + resultField[1].Id.Should().Be(expectedField[1].id.Value); + + TextField? secondField = resultField[1].Fields["textField"].Read(); + secondField!.Value.Should().Be(expectedField[1].fields.textField.value.Value); + secondField.EditableMarkup.Should().Be(expectedField[1].fields.textField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_StrongTypeContentListField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + ContentListField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(32)? + .Fields["sharedContentList"] + .Read>(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][32] + .fields.sharedContentList; + + resultField!.Should().HaveCount(2); + resultField![0].Id.Should().Be(expectedField[0].id.Value); + + TextField firstField = resultField[0].Target!.TextField; + firstField.Value.Should().Be(expectedField[0].fields.textField.value.Value); + firstField.EditableMarkup.Should().Be(expectedField[0].fields.textField.editable.Value); + + resultField[1].Id.Should().Be(expectedField[1].id.Value); + + TextField secondField = resultField[1].Target!.TextField; + secondField.Value.Should().Be(expectedField[1].fields.textField.value.Value); + secondField.EditableMarkup.Should().Be(expectedField[1].fields.textField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_CheckboxField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + CheckboxField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(20)? + .Fields["checkbox"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][20] + .fields.checkbox; + + resultField!.Value.Should().Be(expectedField.value.Value); + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_DateField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + DateField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(23)? + .Fields["dateTime"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][23] + .fields.dateTime; + + resultField!.Value.Should().Be(expectedField.value.Value); + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_FileField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + FileField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(14)? + .Fields["file"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][14] + .fields.file; + + resultField!.Value.Should().NotBeNull(); + + resultField.Value.Description.Should().Be((string)expectedField.value.description); + resultField.Value.DisplayName.Should().Be((string)expectedField.value.displayName); + resultField.Value.Extension.Should().Be((string)expectedField.value.extension); + resultField.Value.Keywords.Should().Be((string)expectedField.value.keywords); + resultField.Value.MimeType.Should().Be((string)expectedField.value.mimeType); + resultField.Value.Name.Should().Be((string)expectedField.value.name); + resultField.Value.Size.Should().Be(long.Parse((string)expectedField.value.size, CultureInfo.InvariantCulture)); + resultField.Value.Src.Should().Be((string)expectedField.value.src); + resultField.Value.Title.Should().Be((string)expectedField.value.title); + + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_ImageField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + ImageField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(11)? + .Fields["sample1"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][11] + .fields.sample1; + + resultField!.Value.Should().NotBeNull(); + resultField.Value.Src.Should().Be((string)expectedField.value.src); + resultField.Value.Alt.Should().Be((string)expectedField.value.alt); + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_NumberField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + NumberField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(17)? + .Fields["sample"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][17] + .fields.sample; + string language = jsonModel.sitecore.context.language; + + resultField!.Value.Should().Be(decimal.Parse(expectedField.value.Value, new CultureInfo(language))); + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } + + [Theory] + [MemberData(nameof(Serializers))] + public void Component_RichTextField_CanBeRead(ISitecoreLayoutSerializer serializer) + { + // Arrange + string json = File.ReadAllText("./Json/edit.json"); + dynamic jsonModel = JObject.Parse(json); + + // Act + SitecoreLayoutResponseContent? result = serializer.Deserialize(json); + + // Assert + RichTextField? resultField = result?.Sitecore?.Route? + .Placeholders["jss-main"].ComponentAt(5)? + .Placeholders["jss-styleguide-layout"].ComponentAt(2)? + .Placeholders["jss-styleguide-section"].ComponentAt(8)? + .Fields["sample"] + .Read(); + + dynamic? expectedField = jsonModel.sitecore.route + .placeholders["jss-main"][5] + .placeholders["jss-styleguide-layout"][2] + .placeholders["jss-styleguide-section"][8] + .fields.sample; + + resultField!.Value.Should().Be(expectedField.value.Value); + resultField.EditableMarkup.Should().Be(expectedField.editable.Value); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/MockModels/LinkFieldModel.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/MockModels/LinkFieldModel.cs new file mode 100644 index 0000000..3a61305 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/MockModels/LinkFieldModel.cs @@ -0,0 +1,8 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.MockModels; + +public class LinkFieldModel +{ + public required TextField TextField { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj new file mode 100644 index 0000000..54bab10 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + Json\edit.json + Always + + + Json\edit-in-horizon-mode.json + Always + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/HttpLayoutRequestHandlerOptionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/HttpLayoutRequestHandlerOptionsFixture.cs new file mode 100644 index 0000000..cba4b72 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/HttpLayoutRequestHandlerOptionsFixture.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Configuration; + +public class HttpLayoutRequestHandlerOptionsFixture +{ + [Fact] + public void Ctor_SetsDefaults() + { + // Arrange / Act + HttpLayoutRequestHandlerOptions sut = new(); + + // Assert + sut.RequestMap.Should().NotBeNull(); + sut.RequestMap.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutClientBuilderFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutClientBuilderFixture.cs new file mode 100644 index 0000000..6c1a23c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutClientBuilderFixture.cs @@ -0,0 +1,31 @@ +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Configuration; + +public class SitecoreLayoutClientBuilderFixture +{ + [Theory] + [AutoNSubstituteData] + public void Ctor_IsGuarded(GuardClauseAssertion guard) + { + // Act / Assert + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_SetsServices(IServiceCollection services) + { + // Arrange / Act + SitecoreLayoutClientBuilder sut = new(services); + + // Assert + sut.Services.Should().BeSameAs(services); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestHandlerBuilderTClientFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestHandlerBuilderTClientFixture.cs new file mode 100644 index 0000000..ecd0d0f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestHandlerBuilderTClientFixture.cs @@ -0,0 +1,52 @@ +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Configuration; + +public class SitecoreLayoutRequestHandlerBuilderTClientFixture +{ + [Theory] + [AutoNSubstituteData] + public void Ctor_IsGuarded(GuardClauseAssertion guard) + { + // Act / Assert + guard.VerifyConstructors>(); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void Ctor_InvalidHandlerName_SetsProperties(string handlerName, IServiceCollection services) + { + // Arrange + Action action = () => _ = new SitecoreLayoutRequestHandlerBuilder(handlerName, services); + + // Assert + action.Should().Throw() + .And.ParamName.Should().Be("handlerName"); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_ValidValues_SetsProperties(IServiceCollection services, string handlerName) + { + // Arrange / Act + SitecoreLayoutRequestHandlerBuilder sut = new(handlerName, services); + + // Assert + sut.Services.Should().BeSameAs(services); + sut.HandlerName.Should().Be(handlerName); + } + + private static IEnumerable EmptyStrings() + { + yield return [null!]; + yield return [string.Empty]; + yield return ["\t\t "]; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestOptionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestOptionsFixture.cs new file mode 100644 index 0000000..1aab3fc --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutRequestOptionsFixture.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Configuration; + +public class SitecoreLayoutRequestOptionsFixture +{ + [Fact] + public void Ctor_SetsDefaults() + { + // Arrange / Act + SitecoreLayoutRequestOptions sut = new(); + + // Assert + sut.RequestDefaults.Should().NotBeNull(); + sut.RequestDefaults.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutServiceOptionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutServiceOptionsFixture.cs new file mode 100644 index 0000000..f0c57a9 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Configuration/SitecoreLayoutServiceOptionsFixture.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Configuration; + +public class SitecoreLayoutServiceOptionsFixture +{ + [Fact] + public void Ctor_SetsDefaults() + { + // Arrange / Act + SitecoreLayoutClientOptions sut = new(); + + // Assert + sut.DefaultHandler.Should().BeNull(); + sut.HandlerRegistry.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/DefaultLayoutClientFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/DefaultLayoutClientFixture.cs new file mode 100644 index 0000000..e0fb038 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/DefaultLayoutClientFixture.cs @@ -0,0 +1,450 @@ +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests; + +public class DefaultLayoutClientFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Freeze>(); + }; + + public static Action HandlerInvocationThrowsException => f => + { + IServiceProvider? services = Substitute.For(); + + Func? handler = Substitute.For>(); + handler.Invoke(Arg.Any()).Throws(new SitecoreLayoutServiceClientException()); + + SitecoreLayoutClientOptions options = new() + { + DefaultHandler = "HandlerDoesNotExist", + HandlerRegistry = new Dictionary> { { "HandlerDoesNotExist", handler } } + }; + + IOptions? configureOptions = f.Freeze>(); + configureOptions.Value.Returns(options); + + IOptionsSnapshot? layoutRequestOptions = Substitute.For>(); + ILogger? logger = Substitute.For>(); + f.Inject(new DefaultLayoutClient(services, configureOptions, layoutRequestOptions, logger)); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithNullRequest_Throws(DefaultLayoutClient sut) + { + // Arrange + Func> act = + () => sut.Request(null!, string.Empty); + + // Act & Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithEmptyHandlerName_Throws( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request) + { + // Arrange + options.Value.DefaultHandler = string.Empty; + Func> act = + () => sut.Request(request, string.Empty); + + // Act & Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithNullRegistryFunc_Throws( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + options.Value.HandlerRegistry.Add(handlerName, null!); + Func> act = + () => sut.Request(request, handlerName); + + // Act / Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithNullHandler_Throws( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + options.Value.HandlerRegistry.Add(handlerName, Func); + Func> act = + () => sut.Request(request, handlerName); + + // Act / Assert + await act.Should().ThrowAsync(); + return; + + static ILayoutRequestHandler Func(IServiceProvider sp) => null!; + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithEmptyRegistry_Throws( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + options.Value.HandlerRegistry.Clear(); + Func> act = + () => sut.Request(request, handlerName); + + // Act / Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithValidRequest_InvokesHandler( + DefaultLayoutClient sut, + SitecoreLayoutRequest request, + IOptions sitecoreLayoutServiceOptions, + string handlerName) + { + // Arrange + ILayoutRequestHandler? sitecoreLayoutService = Substitute.For(); + + Func? handler = Substitute.For>(); + sitecoreLayoutServiceOptions.Value.HandlerRegistry.Clear(); + sitecoreLayoutServiceOptions.Value.HandlerRegistry.Add(handlerName, handler); + + handler.Invoke(Arg.Any()).Returns(sitecoreLayoutService); + + // Act + await sut.Request(request, handlerName); + + // Assert + await sitecoreLayoutService.Received(1).Request(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Request_OptionsHasDefaultHandlerNameAndMethodHandlerNameArgIsNull_RequestUsesOptionsDefaultHandlerName() + { + // Arrange + IServiceProvider? serviceProvider = Substitute.For(); + const string defaultHandlerName = "DefaultHandlerName"; + SitecoreLayoutRequest request = []; + + SitecoreLayoutClientOptions layoutClientOptions = new() { DefaultHandler = defaultHandlerName }; + IOptions? layoutClientOptionsStub = Substitute.For>(); + layoutClientOptionsStub.Value.Returns(layoutClientOptions); + + SitecoreLayoutRequestOptions layoutRequestOptions = new(); + IOptionsSnapshot? layoutRequestOptionsStub = Substitute.For>(); + layoutRequestOptionsStub.Value.Returns(layoutRequestOptions); + layoutRequestOptionsStub.Get(Arg.Is(defaultHandlerName)).Returns(layoutRequestOptions); + ILogger? logger = Substitute.For>(); + + DefaultLayoutClient sut = new(serviceProvider, layoutClientOptionsStub, layoutRequestOptionsStub, logger); + + ILayoutRequestHandler? layoutRequestHandler = Substitute.For(); + + Func? handler = Substitute.For>(); + layoutClientOptions.HandlerRegistry.Clear(); + layoutClientOptions.HandlerRegistry.Add(defaultHandlerName, handler); + + handler.Invoke(Arg.Any()).Returns(layoutRequestHandler); + + // Act + await sut.Request(request, null!); + + // Assert + await layoutRequestHandler.Received(1).Request(Arg.Is(request), Arg.Is(defaultHandlerName)); + } + + [Fact] + public async Task Request_OptionsHasDefaultHandlerNameNameAndMethodHandlerNameArgIsSet_RequestUsesMethodArgHandlerName() + { + // Arrange + IServiceProvider? serviceProvider = Substitute.For(); + const string defaultHandlerName = "DefaultHandlerName"; + const string handlerName = "HandlerName"; + SitecoreLayoutRequest request = []; + + SitecoreLayoutClientOptions layoutClientOptions = new() { DefaultHandler = defaultHandlerName }; + IOptions? layoutClientOptionsStub = Substitute.For>(); + layoutClientOptionsStub.Value.Returns(layoutClientOptions); + + SitecoreLayoutRequestOptions layoutRequestOptions = new(); + IOptionsSnapshot? layoutRequestOptionsStub = Substitute.For>(); + layoutRequestOptionsStub.Value.Returns(layoutRequestOptions); + layoutRequestOptionsStub.Get(Arg.Is(handlerName)).Returns(layoutRequestOptions); + ILogger? logger = Substitute.For>(); + + DefaultLayoutClient sut = new(serviceProvider, layoutClientOptionsStub, layoutRequestOptionsStub, logger); + + ILayoutRequestHandler? layoutRequestHandler = Substitute.For(); + + Func? handler = Substitute.For>(); + layoutClientOptions.HandlerRegistry.Clear(); + layoutClientOptions.HandlerRegistry.Add(defaultHandlerName, handler); + layoutClientOptions.HandlerRegistry.Add(handlerName, handler); + + handler.Invoke(Arg.Any()).Returns(layoutRequestHandler); + + // Act + await sut.Request(request, handlerName); + + // Assert + await layoutRequestHandler.Received(1).Request(Arg.Is(request), Arg.Is(handlerName)); + } + + [Fact] + public async Task Request_OptionsHasNullDefaultHandlerNameNameAndMethodHandlerNameArgIsNull_Throws() + { + // Arrange + IServiceProvider? serviceProvider = Substitute.For(); + SitecoreLayoutRequest request = []; + + SitecoreLayoutClientOptions layoutClientOptions = new() { DefaultHandler = null }; + IOptions? layoutClientOptionsStub = Substitute.For>(); + layoutClientOptionsStub.Value.Returns(layoutClientOptions); + + IOptionsSnapshot? layoutRequestOptions = Substitute.For>(); + ILogger? logger = Substitute.For>(); + + DefaultLayoutClient sut = new(serviceProvider, layoutClientOptionsStub, layoutRequestOptions, logger); + + ILayoutRequestHandler? layoutRequestHandler = Substitute.For(); + + Func? handler = Substitute.For>(); + layoutClientOptions.HandlerRegistry.Clear(); + layoutClientOptions.HandlerRegistry.Add("DefaultClientName", handler); + + handler.Invoke(Arg.Any()).Returns(layoutRequestHandler); + Func> act = + async () => await sut.Request(request, null!); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("Handler name cannot be null."); + } + + [Fact] + public async Task Request_OptionsHasSiteNameAndRequestHasSiteName_RequestSiteNameIsUsed() + { + // Arrange + const string defaultHandlerName = "DefaultHandlerName"; + const string requestSiteName = "requestsitename"; + IServiceProvider? serviceProvider = Substitute.For(); + SitecoreLayoutRequest request = new SitecoreLayoutRequest().SiteName(requestSiteName); + + SitecoreLayoutClientOptions layoutClientOptions = new() + { + DefaultHandler = defaultHandlerName + }; + + SitecoreLayoutRequestOptions layoutRequestOptions = new() + { + RequestDefaults = new SitecoreLayoutRequest + { + { RequestKeys.SiteName, "optionssitename" } + } + }; + + IOptions? layoutClientOptionsStub = Substitute.For>(); + layoutClientOptionsStub.Value.Returns(layoutClientOptions); + + IOptionsSnapshot? layoutRequestOptionsStub = Substitute.For>(); + layoutRequestOptionsStub.Value.Returns(new SitecoreLayoutRequestOptions()); + layoutRequestOptionsStub.Get(Arg.Is(defaultHandlerName)).Returns(layoutRequestOptions); + ILogger? logger = Substitute.For>(); + + DefaultLayoutClient sut = new(serviceProvider, layoutClientOptionsStub, layoutRequestOptionsStub, logger); + + ILayoutRequestHandler? layoutRequestHandler = Substitute.For(); + + Func? handler = Substitute.For>(); + layoutClientOptions.HandlerRegistry.Clear(); + layoutClientOptions.HandlerRegistry.Add(defaultHandlerName, handler); + + handler.Invoke(Arg.Any()).Returns(layoutRequestHandler); + + // Act + await sut.Request(request, defaultHandlerName); + + // Assert + await layoutRequestHandler.Received(1).Request(Arg.Is(x => x.SiteName() == requestSiteName), Arg.Any()); + } + + [Fact] + public async Task Request_OptionsHasSiteNameAndRequestHasNullSiteName_SiteNameIsRemovedFromRequest() + { + // Arrange + string defaultHandlerName = "DefaultHandlerName"; + IServiceProvider? serviceProvider = Substitute.For(); + SitecoreLayoutRequest request = new SitecoreLayoutRequest().SiteName(null); + + SitecoreLayoutClientOptions layoutClientOptions = new() + { + DefaultHandler = defaultHandlerName + }; + + SitecoreLayoutRequestOptions layoutRequestOptions = new() + { + RequestDefaults = new SitecoreLayoutRequest + { + { RequestKeys.SiteName, "optionssitename" } + } + }; + + IOptions? layoutClientOptionsStub = Substitute.For>(); + layoutClientOptionsStub.Value.Returns(layoutClientOptions); + + IOptionsSnapshot? layoutRequestOptionsStub = Substitute.For>(); + layoutRequestOptionsStub.Value.Returns(new SitecoreLayoutRequestOptions()); + layoutRequestOptionsStub.Get(Arg.Is(defaultHandlerName)).Returns(layoutRequestOptions); + ILogger? logger = Substitute.For>(); + + DefaultLayoutClient sut = new(serviceProvider, layoutClientOptionsStub, layoutRequestOptionsStub, logger); + + ILayoutRequestHandler? layoutRequestHandler = Substitute.For(); + + Func? handler = Substitute.For>(); + layoutClientOptions.HandlerRegistry.Clear(); + layoutClientOptions.HandlerRegistry.Add(defaultHandlerName, handler); + + handler.Invoke(Arg.Any()).Returns(layoutRequestHandler); + + // Act + await sut.Request(request, defaultHandlerName); + + // Assert + await layoutRequestHandler.Received(1).Request(Arg.Is(x => !x.ContainsKey(RequestKeys.SiteName)), Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task RequestMethod_Returns_Propagated_CouldNotContactSitecoreLayoutServiceClientException( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + ILayoutRequestHandler? sitecoreLayoutService = Substitute.For(); + + List errors = + [new CouldNotContactSitecoreLayoutServiceClientException()]; + SitecoreLayoutResponse responseWithErrors = new(request, errors); + sitecoreLayoutService.Request(Arg.Any(), Arg.Any()).Returns(responseWithErrors); + + Func? handler = Substitute.For>(); + handler.Invoke(Arg.Any()).Returns(sitecoreLayoutService); + + options.Value.HandlerRegistry.Add(handlerName, handler); + + // Act + SitecoreLayoutResponse response = await sut.Request(request, handlerName); + + // Assert + response.Should().NotBeNull(); + response.HasErrors.Should().BeTrue(); + response.Errors.Should().ContainSingle(x => x.GetType() == typeof(CouldNotContactSitecoreLayoutServiceClientException)); + } + + [Theory] + [AutoNSubstituteData] + public async Task RequestMethod_Returns_Propagated_InvalidResponseSitecoreLayoutServiceClientException( + DefaultLayoutClient sut, + IOptions options, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + ILayoutRequestHandler? sitecoreLayoutService = Substitute.For(); + + List errors = + [new InvalidResponseSitecoreLayoutServiceClientException()]; + SitecoreLayoutResponse responseWithErrors = new(request, errors); + sitecoreLayoutService.Request(Arg.Any(), Arg.Any()).Returns(responseWithErrors); + + Func? handler = Substitute.For>(); + handler.Invoke(Arg.Any()).Returns(sitecoreLayoutService); + + options.Value.HandlerRegistry.Add(handlerName, handler); + + // Act + SitecoreLayoutResponse response = await sut.Request(request, handlerName); + + // Assert + response.HasErrors.Should().BeTrue(); + response.Errors.Should().ContainSingle(x => x.GetType() == typeof(InvalidResponseSitecoreLayoutServiceClientException)); + } + + [Theory] + [AutoNSubstituteData(nameof(HandlerInvocationThrowsException))] + public void RequestMethod_WithInvalidHandlerName_Throws( + DefaultLayoutClient sut, + SitecoreLayoutRequest request, + IOptions layoutClientOptions) + { + // Arrange + layoutClientOptions.Value.HandlerRegistry.Clear(); + + // Act + Func action = async () => await sut.Request(request, "HandlerDoesNotExist"); + + // Assert + action.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public void RequestMethod_Throws_Propagated_UnhandledException( + DefaultLayoutClient sut, + SitecoreLayoutRequest request, + string handlerName) + { + // Arrange + Func? handler = Substitute.For>(); + + handler.Invoke(Arg.Any()).Throws(new Exception()); + + // Act + Func action = async () => await sut.Request(request, handlerName); + + // Assert + action.Should().ThrowAsync(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/CouldNotContactSitecoreLayoutServiceClientExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/CouldNotContactSitecoreLayoutServiceClientExceptionFixture.cs new file mode 100644 index 0000000..7f550e6 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/CouldNotContactSitecoreLayoutServiceClientExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class CouldNotContactSitecoreLayoutServiceClientExceptionFixture +{ + private const string DefaultMessage = "Could not contact the Sitecore layout service."; + + [Theory] + [AutoNSubstituteData] + public void CouldNotContactSitecoreLayoutServiceClientException_WithMessage_SetsMessage(string message) + { + // Act + CouldNotContactSitecoreLayoutServiceClientException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void CouldNotContactSitecoreLayoutServiceClientException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + CouldNotContactSitecoreLayoutServiceClientException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void CouldNotContactSitecoreLayoutServiceClientException_WithNoMessage_UsesDefaultMessage() + { + // Act + CouldNotContactSitecoreLayoutServiceClientException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void CouldNotContactSitecoreLayoutServiceClientException_WithException_SetsInnerException( + Exception exception) + { + // Act + CouldNotContactSitecoreLayoutServiceClientException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/FieldReaderExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/FieldReaderExceptionFixture.cs new file mode 100644 index 0000000..30a16f2 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/FieldReaderExceptionFixture.cs @@ -0,0 +1,61 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class FieldReaderExceptionFixture +{ + private const string DefaultMessage = "The Field could not be read as the type {0}"; + + [Theory] + [AutoNSubstituteData] + public void Ctor_WithMessage_SetsMessage(string message) + { + // Act + FieldReaderException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + FieldReaderException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void Ctor_WithType_UsesDefaultMessage() + { + // Act + Type type = typeof(int); + FieldReaderException sut = new(type); + + // Assert + sut.Message.Should().Be(string.Format(System.Globalization.CultureInfo.CurrentCulture, DefaultMessage, type)); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_WithException_SetsInnerException( + Exception exception) + { + // Act + Type type = typeof(int); + FieldReaderException sut = new(type, exception); + + // Assert + sut.Message.Should().Be(string.Format(System.Globalization.CultureInfo.CurrentCulture, DefaultMessage, type)); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidRequestSitecoreLayoutServiceClientExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidRequestSitecoreLayoutServiceClientExceptionFixture.cs new file mode 100644 index 0000000..407e87d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidRequestSitecoreLayoutServiceClientExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class InvalidRequestSitecoreLayoutServiceClientExceptionFixture +{ + private const string DefaultMessage = "An invalid request was sent to the Sitecore layout service."; + + [Theory] + [AutoNSubstituteData] + public void InvalidRequestSitecoreLayoutServiceClientException_WithMessage_SetsMessage(string message) + { + // Act + InvalidRequestSitecoreLayoutServiceClientException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void InvalidRequestSitecoreLayoutServiceClientException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + InvalidRequestSitecoreLayoutServiceClientException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void InvalidRequestSitecoreLayoutServiceClientException_WithNoMessage_UsesDefaultMessage() + { + // Act + InvalidRequestSitecoreLayoutServiceClientException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void InvalidRequestSitecoreLayoutServiceClientException_WithException_SetsInnerException( + Exception exception) + { + // Act + InvalidRequestSitecoreLayoutServiceClientException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidResponseSitecoreLayoutServiceClientExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidResponseSitecoreLayoutServiceClientExceptionFixture.cs new file mode 100644 index 0000000..cd2e616 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/InvalidResponseSitecoreLayoutServiceClientExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class InvalidResponseSitecoreLayoutServiceClientExceptionFixture +{ + private const string DefaultMessage = "The Sitecore layout service returned a response in an invalid format."; + + [Theory] + [AutoNSubstituteData] + public void InvalidResponseSitecoreLayoutServiceClientException_WithMessage_SetsMessage(string message) + { + // Act + InvalidResponseSitecoreLayoutServiceClientException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void InvalidResponseSitecoreLayoutServiceClientException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + InvalidResponseSitecoreLayoutServiceClientException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void InvalidResponseSitecoreLayoutServiceClientException_WithNoMessage_UsesDefaultMessage() + { + // Act + InvalidResponseSitecoreLayoutServiceClientException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void InvalidResponseSitecoreLayoutServiceClientException_WithException_SetsInnerException( + Exception exception) + { + // Act + InvalidResponseSitecoreLayoutServiceClientException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/ItemNotFoundSitecoreLayoutServiceClientExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/ItemNotFoundSitecoreLayoutServiceClientExceptionFixture.cs new file mode 100644 index 0000000..8752f45 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/ItemNotFoundSitecoreLayoutServiceClientExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class ItemNotFoundSitecoreLayoutServiceClientExceptionFixture +{ + private const string DefaultMessage = "The Sitecore layout service returned an item not found response."; + + [Theory] + [AutoNSubstituteData] + public void ItemNotFoundSitecoreLayoutServiceClientException_WithMessage_SetsMessage(string message) + { + // Act + ItemNotFoundSitecoreLayoutServiceClientException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void ItemNotFoundSitecoreLayoutServiceClientException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + ItemNotFoundSitecoreLayoutServiceClientException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void ItemNotFoundSitecoreLayoutServiceClientException_WithNoMessage_UsesDefaultMessage() + { + // Act + ItemNotFoundSitecoreLayoutServiceClientException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void ItemNotFoundSitecoreLayoutServiceClientException_WithException_SetsInnerException( + Exception exception) + { + // Act + ItemNotFoundSitecoreLayoutServiceClientException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceClientExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceClientExceptionFixture.cs new file mode 100644 index 0000000..06c2f5d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceClientExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class SitecoreLayoutServiceClientExceptionFixture +{ + private const string DefaultMessage = "An error occurred with the Sitecore layout service."; + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceClientException_WithMessage_SetsMessage(string message) + { + // Act + SitecoreLayoutServiceClientException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceClientException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + SitecoreLayoutServiceClientException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void SitecoreLayoutServiceClientException_WithNoMessage_UsesDefaultMessage() + { + // Act + SitecoreLayoutServiceClientException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceClientException_WithException_SetsInnerException( + Exception exception) + { + // Act + SitecoreLayoutServiceClientException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceMessageConfigurationExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceMessageConfigurationExceptionFixture.cs new file mode 100644 index 0000000..0b59eaa --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceMessageConfigurationExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class SitecoreLayoutServiceMessageConfigurationExceptionFixture +{ + private const string DefaultMessage = "An error occurred while configuring the HTTP message."; + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceMessageConfigurationException_WithMessage_SetsMessage(string message) + { + // Act + SitecoreLayoutServiceMessageConfigurationException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceMessageConfigurationException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + SitecoreLayoutServiceMessageConfigurationException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void SitecoreLayoutServiceMessageConfigurationException_WithNoMessage_UsesDefaultMessage() + { + // Act + SitecoreLayoutServiceMessageConfigurationException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceMessageConfigurationException_WithException_SetsInnerException( + Exception exception) + { + // Act + SitecoreLayoutServiceMessageConfigurationException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceServerExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceServerExceptionFixture.cs new file mode 100644 index 0000000..93fb7d6 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/SitecoreLayoutServiceServerExceptionFixture.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; + +public class SitecoreLayoutServiceServerExceptionFixture +{ + private const string DefaultMessage = "The Sitecore layout service returned a server error."; + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceServerException_WithMessage_SetsMessage(string message) + { + // Act + SitecoreLayoutServiceServerException sut = new(message); + + // Assert + sut.Message.Should().Be(message); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceServerException_WithMessageAndException_SetsMessageAndInnerException( + string message, + Exception exception) + { + // Act + SitecoreLayoutServiceServerException sut = new(message, exception); + + // Assert + sut.Message.Should().Be(message); + sut.InnerException.Should().Be(exception); + } + + [Fact] + public void SitecoreLayoutServiceServerException_WithNoMessage_UsesDefaultMessage() + { + // Act + SitecoreLayoutServiceServerException sut = new(); + + // Assert + sut.Message.Should().Be(DefaultMessage); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreLayoutServiceServerException_WithException_SetsInnerException( + Exception exception) + { + // Act + SitecoreLayoutServiceServerException sut = new(exception); + + // Assert + sut.Message.Should().Be(DefaultMessage); + sut.InnerException.Should().Be(exception); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/DictionaryExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/DictionaryExtensionsFixture.cs new file mode 100644 index 0000000..98a01b5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/DictionaryExtensionsFixture.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions; + +public class DictionaryExtensionsFixture +{ + [Fact] + public void ToDebugString_DictionaryIsNull_ThrowsArgumentNullException() + { + // Arrange + Func act = + () => ((Dictionary)null!).ToDebugString(); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'dictionary')"); + } + + [Fact] + public void ToDebugString_EmptyDictionary_ReturnBrackets() + { + // Act + Dictionary testsDictionary = []; + string result = testsDictionary.ToDebugString(); + + // Assert + result.Should().Be("{}"); + } + + [Fact] + public void ToDebugString_NotEmptyDictionary_ReturnString() + { + // Act + Dictionary testsDictionary = new() + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + string result = testsDictionary.ToDebugString(); + + // Assert + result.Should().Be("{key1=value1,key2=value2}"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/HttpLayoutRequestHandlerBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/HttpLayoutRequestHandlerBuilderExtensionsFixture.cs new file mode 100644 index 0000000..c1b3961 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/HttpLayoutRequestHandlerBuilderExtensionsFixture.cs @@ -0,0 +1,328 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions.Http; + +public class HttpLayoutRequestHandlerBuilderExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ServiceCollection services = []; + f.Inject(services); + + ISitecoreLayoutSerializer? serializer = f.Create(); + services.AddSingleton(serializer); + + IHttpClientFactory? httpClientFactory = f.Freeze(); + services.AddSingleton(httpClientFactory); + + ILogger? logger = f.Freeze>(); + services.AddSingleton(logger); + }; + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler1a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, resolveClient); + Func> handlerNull = + () => builder.AddHttpHandler(null!, resolveClient); + Func> resolveClientNull = () => + builder.AddHttpHandler(handlerName, (Func)null!); + + // Assert + builderNull.Should().Throw(); + handlerNull.Should().Throw(); + resolveClientNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler1a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient) + { + // Arrange & Act + builder.AddHttpHandler(handlerName, resolveClient); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler1a_Adds_DefaultSitecoreRequestMapping(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient) + { + // Arrange & Act + ILayoutRequestHandlerBuilder clientBuilder = builder.AddHttpHandler(handlerName, resolveClient); + + ServiceProvider sp = clientBuilder.Services.BuildServiceProvider(); + IOptionsSnapshot? handlerOptions = sp.GetService>(); + HttpLayoutRequestHandlerOptions namedHandlerOptions = handlerOptions!.Get(handlerName); + + // Assert + namedHandlerOptions.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().ContainSingle(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler2a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, Action resolveClient) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, resolveClient); + Func> handlerNameNull = + () => builder.AddHttpHandler(null!, resolveClient); + Func> configure = + () => builder.AddHttpHandler(handlerName, (Action)null!); + + // Assert + builderNull.Should().Throw(); + handlerNameNull.Should().Throw(); + configure.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler2a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName, Action resolveClient) + { + // Arrange & Act + builder.AddHttpHandler(handlerName, resolveClient); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler3a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, Action configureClient) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, configureClient); + Func> handlerNameNull = + () => builder.AddHttpHandler(null!, configureClient); + Func> configureNull = + () => builder.AddHttpHandler(handlerName, (Action)null!); + + // Assert + builderNull.Should().Throw(); + handlerNameNull.Should().Throw(); + configureNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler3a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName, Action resolveClient) + { + // Arrange & Act + builder.AddHttpHandler(handlerName, resolveClient); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler4a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, Uri uri) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, uri); + Func> handlerNameNull = + () => builder.AddHttpHandler(null!, uri); + Func> uriNull = + () => builder.AddHttpHandler(handlerName, (Uri)null!); + + // Assert + builderNull.Should().Throw(); + handlerNameNull.Should().Throw(); + uriNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler4a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName, Uri uri) + { + // Arrange & Act + builder.AddHttpHandler(handlerName, uri); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler5a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, string uri) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, uri); + Func> handlerNameNull = + () => builder.AddHttpHandler(null!, uri); + Func> uriStringNull = + () => builder.AddHttpHandler(handlerName, (string)null!); + + // Assert + builderNull.Should().Throw(); + handlerNameNull.Should().Throw(); + uriStringNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler5a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName) + { + // Arrange + const string uri = "http://www.test.com"; + + // Act + builder.AddHttpHandler(handlerName, uri); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler6a_IsGuarded(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient) + { + // Arrange + Func> builderNull = + () => SitecoreLayoutClientBuilderExtensions.AddHttpHandler(null!, handlerName, resolveClient, []); + Func> handlerNameNull = + () => builder.AddHttpHandler(null!, resolveClient, []); + Func> resolveClientNull = + () => builder.AddHttpHandler(handlerName, null!, []); + Func> nonValidatedHeadersNull = + () => builder.AddHttpHandler(handlerName, resolveClient, null!); + + // Assert + builderNull.Should().Throw(); + handlerNameNull.Should().Throw(); + resolveClientNull.Should().Throw(); + nonValidatedHeadersNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler6a_Returns_HttpLayoutRequestHandler(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient, string[] nonValidatedHeaders) + { + // Arrange & Act + builder.AddHttpHandler(handlerName, resolveClient, nonValidatedHeaders); + + ServiceProvider provider = builder.Services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); + ILayoutRequestHandler result = options!.Value.HandlerRegistry[handlerName].Invoke(provider); + + // Assert + result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHttpHandler6a_Adds_DefaultSitecoreRequestMapping(ISitecoreLayoutClientBuilder builder, string handlerName, Func resolveClient, string[] nonValidatedHeaders) + { + // Arrange & Act + ILayoutRequestHandlerBuilder clientBuilder = builder.AddHttpHandler(handlerName, resolveClient, nonValidatedHeaders); + + ServiceProvider sp = clientBuilder.Services.BuildServiceProvider(); + IOptionsSnapshot? handlerOptions = sp.GetService>(); + HttpLayoutRequestHandlerOptions namedHandlerOptions = handlerOptions!.Get(handlerName); + + // Assert + namedHandlerOptions.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().ContainSingle(); + } + + [Theory] + [AutoNSubstituteData] + public void MapFromRequest_IsGuarded(ILayoutRequestHandlerBuilder builder, Action configureHttpRequestMessage) + { + // Arrange + Func> builderNull = + () => LayoutRequestHandlerBuilderExtensions.MapFromRequest(null!, configureHttpRequestMessage); + Func> configureHttpRequestMessageNull = + () => builder.MapFromRequest(null!); + Func> allNull = + () => LayoutRequestHandlerBuilderExtensions.MapFromRequest(null!, null!); + + // Assert + builderNull.Should().Throw(); + configureHttpRequestMessageNull.Should().Throw(); + allNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void MapFromRequest_WithValidAction_AddsHttpLayoutRequestHandlerOption(ISitecoreLayoutClientBuilder clientBuilder, string handlerName) + { + // Arrange + ILayoutRequestHandlerBuilder handlerBuilder = clientBuilder.AddHttpHandler(handlerName, "http://www.test.com"); + + // Act + handlerBuilder.MapFromRequest(ConfigureHttpRequestMessage); + ServiceProvider sp = clientBuilder.Services.BuildServiceProvider(); + IOptionsSnapshot? handlerOptions = sp.GetService>(); + HttpLayoutRequestHandlerOptions namedHandlerOptions = handlerOptions!.Get(handlerName); + + // Assert + namedHandlerOptions.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().NotBeNull(); + namedHandlerOptions.RequestMap.Should().HaveCount(2); + return; + + static void ConfigureHttpRequestMessage(SitecoreLayoutRequest request, HttpRequestMessage message) => message.Method = HttpMethod.Post; + } + + [Theory] + [AutoNSubstituteData] + public void ConfigureRequest_IsGuarded(ILayoutRequestHandlerBuilder handler, string[] nonValidatedHeaders) + { + // Arrange + Func> httpHandlerBuilderNull = + () => LayoutRequestHandlerBuilderExtensions.ConfigureRequest(null!, nonValidatedHeaders); + Func> nonValidatedHeadersNull = + () => handler.ConfigureRequest(null!); + + // Assert + httpHandlerBuilderNull.Should().Throw(); + nonValidatedHeadersNull.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/SitecoreLayoutRequestHttpExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/SitecoreLayoutRequestHttpExtensionsFixture.cs new file mode 100644 index 0000000..2defa23 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/Http/SitecoreLayoutRequestHttpExtensionsFixture.cs @@ -0,0 +1,147 @@ +using System.Net; +using AutoFixture; +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Xunit; +using SitecoreLayoutRequestExtensions = Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions.SitecoreLayoutRequestExtensions; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions.Http; + +public class SitecoreLayoutRequestHttpExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Inject(new SitecoreLayoutRequest()); + }; + + [Theory] + [AutoNSubstituteData] + public void BuildDefaultSitecoreLayoutRequestUri_IsGuarded(SitecoreLayoutRequest request, Uri baseAddress) + { + // Arrange + Func allNull = + () => SitecoreLayoutRequestExtensions.BuildDefaultSitecoreLayoutRequestUri(null!, null!); + Func uriNull = + () => request.BuildDefaultSitecoreLayoutRequestUri(null!); + Func requestNull = + () => SitecoreLayoutRequestExtensions.BuildDefaultSitecoreLayoutRequestUri(null!, baseAddress); + + // Assert + allNull.Should().Throw(); + uriNull.Should().Throw(); + requestNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void BuildUri_IsGuarded(SitecoreLayoutRequest request, Uri baseAddress, List uriArgs) + { + // Arrange + Func requestNull = + () => SitecoreLayoutRequestExtensions.BuildUri(null!, null!, null!); + Func allNull = + () => request.BuildUri(null!, null!); + Func queryParamsNull = + () => request.BuildUri(baseAddress, null!); + Func uriNull = + () => request.BuildUri(null!, uriArgs); + + // Assert + requestNull.Should().Throw(); + allNull.Should().Throw(); + queryParamsNull.Should().Throw(); + uriNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void BuildDefaultSitecoreLayoutRequestUri_WithValidRequestContainingAllScEntries_ReturnsCorrectUrl(SitecoreLayoutRequest sut, Uri baseAddress) + { + // Arrange + sut.ApiKey("apikey"); + sut.Language("en"); + sut.Path("home"); + sut.SiteName("site"); + sut.Mode("edit"); + sut.PreviewDate("121212"); + + // Act + Uri uri = sut.BuildDefaultSitecoreLayoutRequestUri(baseAddress); + + // Assert + uri.Should().Be($"{baseAddress}?sc_apikey=apikey&sc_lang=en&item=home&sc_site=site&sc_mode=edit&sc_date=121212"); + } + + [Theory] + [AutoNSubstituteData] + public void BuildDefaultSitecoreLayoutRequestUri_WithValidRequestContainingVariousEntries_ReturnsCorrectUrl(SitecoreLayoutRequest sut, Uri baseAddress) + { + // Arrange + sut.ApiKey("apikey"); + sut.Language("en"); + sut.Add("test1", "testvalue1"); + sut.Add("test2", "testvalue2"); + + // Act + Uri uri = sut.BuildDefaultSitecoreLayoutRequestUri(baseAddress); + + // Assert + uri.Should().Be($"{baseAddress}?sc_apikey=apikey&sc_lang=en"); + } + + [Theory] + [AutoNSubstituteData] + public void BuildDefaultSitecoreLayoutRequestUri_WithValidRequestContainingVariousEntriesAndAdditionalParams_ReturnsCorrectUrl(SitecoreLayoutRequest sut, Uri baseAddress) + { + // Arrange + sut.ApiKey("apikey"); + sut.Language("en"); + sut.Add("test1", "testvalue1"); + sut.Add("test2", "testvalue2"); + + // Act + Uri uri = sut.BuildDefaultSitecoreLayoutRequestUri(baseAddress, ["test1", "test2"]); + + // Assert + uri.Should().Be($"{baseAddress}?sc_apikey=apikey&sc_lang=en&test1=testvalue1&test2=testvalue2"); + } + + [Theory] + [AutoNSubstituteData] + public void BuildUri_WithInvalidRequestEntries_ReturnsCorrectUrl(SitecoreLayoutRequest sut, Uri baseAddress) + { + // Arrange + sut.ApiKey(null); + sut.Language(string.Empty); + sut.Path(string.Empty); + sut.Add(RequestKeys.SiteName, new Cookie()); + List defaultKeys = + [RequestKeys.SiteName, RequestKeys.Path, RequestKeys.Language, RequestKeys.ApiKey]; + + // Act + Uri uri = sut.BuildUri(baseAddress, defaultKeys); + + // Assert + uri.Should().Be($"{baseAddress}"); + } + + [Theory] + [AutoNSubstituteData] + public void BuildUri_WithValidRequestContainingScUnencodedEntries_ReturnsCorrectEncodedUrl(SitecoreLayoutRequest sut, Uri baseAddress) + { + // Arrange + sut.ApiKey(""); + List defaultKeys = + [RequestKeys.SiteName, RequestKeys.Path, RequestKeys.Language, RequestKeys.ApiKey]; + + // Act + Uri uri = sut.BuildUri(baseAddress, defaultKeys); + + // Assert + uri.Should().Be($"{baseAddress}?sc_apikey=alert(\"hello\")%3B<%2Fscript>"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/ServiceCollectionExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/ServiceCollectionExtensionsFixture.cs new file mode 100644 index 0000000..82817f5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/ServiceCollectionExtensionsFixture.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions; + +public class ServiceCollectionExtensionsFixture +{ + [Fact] + public void AddSitecoreLayoutService_ServiceCollectionIsNull_ThrowsArgumentNullException() + { + // Arrange + Func act = + () => ServiceCollectionExtensions.AddSitecoreLayoutService(null!); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'services')"); + } + + [Fact] + public void AddSitecoreLayoutService_ServiceCollection_Contains_ExpectedServices() + { + // Arrange + ServiceCollection services = []; + + // Act + services.AddSitecoreLayoutService(); + + // Assert + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(ISitecoreLayoutClient) && serviceDescriptor.Lifetime == ServiceLifetime.Transient); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(ISitecoreLayoutSerializer) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs new file mode 100644 index 0000000..aaced68 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs @@ -0,0 +1,174 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions; + +public class SitecoreLayoutClientBuilderExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutClientBuilder builder = new(new ServiceCollection()); + f.Inject(builder); + }; + + [Fact] + public void AddHandler_NullBuilder_Throws() + { + // Arrange + Action action = () => SitecoreLayoutClientBuilderExtensions.AddHandler(null!, "string"); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("builder"); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void AddHandler_InvalidName_Throws(string value, SitecoreLayoutClientBuilder builder) + { + // Arrange + Action action = () => builder.AddHandler(value); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("name"); + } + + [Theory] + [AutoNSubstituteData] + public void AddHandler_Throws_IfTServiceIsAnInterface(SitecoreLayoutClientBuilder builder, string handlerName) + { + // Arrange + Action action = () => builder.AddHandler(handlerName); + + // Act / Assert + action.Should().Throw() + .WithMessage($"Can only register implementations of {typeof(ILayoutRequestHandler)} as layout services."); + } + + [Theory] + [AutoNSubstituteData] + public void AddHandler_Throws_IfTServiceIsAnAbstractClass(SitecoreLayoutClientBuilder builder, string handlerName) + { + // Arrange + Action action = () => builder.AddHandler(handlerName); + + // Act / Assert + action.Should().Throw() + .WithMessage("Abstract registrations must provide a factory to resolve a layout service."); + } + + [Theory] + [AutoNSubstituteData] + public void AddHandler_CreatesFactory_IfFactoryIsNull(SitecoreLayoutClientBuilder builder, string handlerName) + { + // Arrange / Act + ILayoutRequestHandlerBuilder result = builder.AddHandler(handlerName); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + SitecoreLayoutClientOptions options = provider.GetRequiredService>().Value; + options.HandlerRegistry[handlerName].Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void AddHandler_UsesTheProvidedFactory_IfFactoryIsNotNull( + SitecoreLayoutClientBuilder builder, + string handlerName, + HttpLayoutRequestHandler service) + { + // Arrange / Act + ILayoutRequestHandlerBuilder result = builder.AddHandler(handlerName, _ => service); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + SitecoreLayoutClientOptions options = provider.GetRequiredService>().Value; + ILayoutRequestHandler instance = options.HandlerRegistry[handlerName].Invoke(provider); + instance.Should().BeSameAs(service); + } + + [Theory] + [AutoNSubstituteData] + public void AddHandler_WithValidValues_ReturnsNewBuilderWithCorrectValues( + SitecoreLayoutClientBuilder builder, + string handlerName) + { + // Arrange / Act + ILayoutRequestHandlerBuilder result = builder.AddHandler(handlerName); + + // Assert + result.Should().BeOfType>(); + result.Services.Should().BeSameAs(builder.Services); + result.HandlerName.Should().Be(handlerName); + } + + [Fact] + public void WithDefaultRequestOptions_BuilderIsNUll_ThrowsArgumentNullException() + { + // Arrange + Func act = + () => SitecoreLayoutClientBuilderExtensions.WithDefaultRequestOptions(null!, null!); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'builder')"); + } + + [Theory] + [AutoNSubstituteData] + public void WithDefaultRequestOptions_ConfigureActionIsNUll_ThrowsArgumentNullException(SitecoreLayoutClientBuilder builder) + { + // Arrange + Func act = + () => builder.WithDefaultRequestOptions(null!); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'configureRequest')"); + } + + [Theory] + [AutoNSubstituteData] + public void WithDefaultRequestOptions_RequestDefaultsIsNotNull_ServiceProviderReturnsOptionsWithRequestDefaults(SitecoreLayoutClientBuilder builder) + { + // Act + ISitecoreLayoutClientBuilder result = builder.WithDefaultRequestOptions(_ => { }); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + SitecoreLayoutRequestOptions options = provider.GetRequiredService>().Value; + options.RequestDefaults.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void WithDefaultRequestOptions_RequestDefaultsHasApiKeySpecified_ServiceProviderReturnsOptionsWithRequestDefaultsContainingApiKey(SitecoreLayoutClientBuilder builder) + { + // Act + ISitecoreLayoutClientBuilder result = builder.WithDefaultRequestOptions(o => { o.ApiKey("test_api_key"); }); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + SitecoreLayoutRequestOptions options = provider.GetRequiredService>().Value; + options.RequestDefaults.Should().NotBeNull(); + options.RequestDefaults.Should().BeOfType(); + options.RequestDefaults.Should().ContainKey(RequestKeys.ApiKey); + options.RequestDefaults.ApiKey().Should().Be("test_api_key"); + } + + private static IEnumerable EmptyStrings() + { + yield return [null!]; + yield return [string.Empty]; + yield return ["\t\t "]; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutRequestHandlerBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutRequestHandlerBuilderExtensionsFixture.cs new file mode 100644 index 0000000..20fec94 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutRequestHandlerBuilderExtensionsFixture.cs @@ -0,0 +1,96 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions; + +public class SitecoreLayoutRequestHandlerBuilderExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ServiceCollection services = []; + f.Inject(services); + + SitecoreLayoutRequestHandlerBuilder clientBuilder = new("testing", new ServiceCollection()); + f.Inject(clientBuilder); + }; + + [Fact] + public void AsDefaultHandler_NullBuilder_Throws() + { + // Arrange + Action action = () => LayoutRequestHandlerBuilderExtensions.AsDefaultHandler(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("builder"); + } + + [Theory] + [AutoNSubstituteData] + public void AsDefaultHandler_WithBuilder_ConfiguresDefault(SitecoreLayoutRequestHandlerBuilder builder) + { + // Arrange / Act + ILayoutRequestHandlerBuilder result = builder.AsDefaultHandler(); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + SitecoreLayoutClientOptions options = provider.GetRequiredService>().Value; + options.DefaultHandler.Should().Be(builder.HandlerName); + } + + [Fact] + public void WithRequestOptions_BuilderIsNull_ThrowsArgumentNullException() + { + // Arrange + ILayoutRequestHandlerBuilder builder = null!; + Func> act = + () => builder.WithRequestOptions(null!); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'builder')"); + } + + [Fact] + public void WithRequestOptions_ActionIsNull_ThrowsArgumentNullException() + { + // Arrange + ServiceCollection services = []; + SitecoreLayoutRequestHandlerBuilder builder = new("test", services); + Func> act = + () => builder.WithRequestOptions(null!); + + // Arrange & Act + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'configureRequest')"); + } + + [Theory] + [AutoNSubstituteData] + public void WithRequestOptions_RequestDefaultsHasApiKeySpecified_ServiceProviderReturnsOptionsWithRequestDefaultsContainingApiKey(IServiceCollection services, string handlerName) + { + // Arrange + SitecoreLayoutRequestHandlerBuilder builder = new(handlerName, services); + + // Act + ILayoutRequestHandlerBuilder result = builder.WithRequestOptions(o => { o.ApiKey("test_api_key"); }); + + // Assert + ServiceProvider provider = result.Services.BuildServiceProvider(); + IOptionsSnapshot? options = provider.GetService>(); + + options.Should().NotBeNull(); + options!.Value.Should().NotBeNull(); + options.Value.Should().BeOfType(); + options.Get(handlerName).RequestDefaults.Should().ContainKey(RequestKeys.ApiKey); + options.Get(handlerName).RequestDefaults.ApiKey().Should().Be("test_api_key"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/TestAbstractSitecoreLayoutRequestHandler.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/TestAbstractSitecoreLayoutRequestHandler.cs new file mode 100644 index 0000000..4f70fcd --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/TestAbstractSitecoreLayoutRequestHandler.cs @@ -0,0 +1,11 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Extensions; + +public abstract class TestAbstractSitecoreLayoutRequestHandler : ILayoutRequestHandler +{ + /// + public abstract Task Request(SitecoreLayoutRequest request, string handlerName); +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpLayoutRequestHandlerWrapper.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpLayoutRequestHandlerWrapper.cs new file mode 100644 index 0000000..dcfca6c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpLayoutRequestHandlerWrapper.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.MockModels; + +public class HttpLayoutRequestHandlerWrapper : HttpLayoutRequestHandler +{ + public HttpLayoutRequestHandlerWrapper(HttpClient client, ISitecoreLayoutSerializer serializer, IOptionsSnapshot options, ILogger logger) + : base(client, serializer, options, logger) + { + // Add a default success response + Responses.Push(new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent("JSON") + }); + } + + public HttpRequestMessage? RequestMessage { get; private set; } + + public Stack Responses { get; } = new(); + + protected override Task GetResponseAsync(HttpRequestMessage requestMessage) + { + RequestMessage = requestMessage; + + HttpResponseMessage response = Responses.Pop(); + + response.RequestMessage = requestMessage; + + return Task.FromResult(response); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpMessageHandlerWrapper.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpMessageHandlerWrapper.cs new file mode 100644 index 0000000..8db5fbe --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpMessageHandlerWrapper.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.MockModels; + +public class HttpMessageHandlerWrapper : HttpMessageHandler +{ + public List Messages { get; } = []; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Messages.Add(request); + return Task.FromResult(new HttpResponseMessage()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpResponseMessageWrapper.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpResponseMessageWrapper.cs new file mode 100644 index 0000000..f73d038 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/MockModels/HttpResponseMessageWrapper.cs @@ -0,0 +1,10 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.MockModels; + +public class HttpResponseMessageWrapper(HttpStatusCode statusCode) + : HttpResponseMessage(statusCode) +{ + public new HttpResponseHeaders? Headers { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs new file mode 100644 index 0000000..c5529c5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs @@ -0,0 +1,229 @@ +using System.Text.Json; +using AutoFixture.Xunit2; +using FluentAssertions; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Request.Handlers; + +public class GraphQlLayoutServiceHandlerFixture +{ + private readonly IGraphQLClient _client; + private readonly ISitecoreLayoutSerializer _serializer; + private readonly GraphQlLayoutServiceHandler _graphQlLayoutServiceHandler; + + public GraphQlLayoutServiceHandlerFixture() + { + _client = Substitute.For(); + _serializer = Substitute.For(); + ILogger? logger = Substitute.For>(); + _graphQlLayoutServiceHandler = new GraphQlLayoutServiceHandler(_client, _serializer, logger); + } + + [Theory] + [AutoData] + public async Task Request_Should_CallSendQueryAsync(string name, string apiKey, string language, string handlerName) + { + // Arrange + const string json = "{\"sitecore\": {\"itemId\": \"44ce10f1-325a-4c75-b026-b68c59d86971\"}}"; + SitecoreLayoutResponseContent resultContent = new(); + LayoutQueryResponse layoutQueryResponse = new() + { + Layout = new LayoutModel + { + Item = new ItemModel + { + Rendered = JsonSerializer.SerializeToElement(json) + } + } + }; + GraphQLResponse response = new() + { + Data = layoutQueryResponse + }; + + SitecoreLayoutRequest request = []; + request + .SiteName(name) + .ApiKey(apiKey); + + request.Add("sc_lang", language); + + string query = @" + query LayoutQuery($path: String!, $language: String!, $site: String!) { + layout(routePath: $path, language: $language, site: $site) { + item { + rendered + } + } + }"; + + _client.SendQueryAsync(Arg.Any()).Returns(response); + + string? data = response.Data.Layout?.Item?.Rendered.ToString(); + _serializer.Deserialize(data ?? string.Empty).Returns(resultContent); + + // Act + await _graphQlLayoutServiceHandler.Request(request, handlerName); + + // Assert + await _client.Received().SendQueryAsync(Arg.Is(r + => r.Query!.Equals(query, StringComparison.Ordinal))); + } + + [Theory] + [AutoData] + public async Task Request_Should_CallDeserialize(string name, string apiKey, string language, string handlerName) + { + // Arrange + string json = " {\"sitecore\": {\"itemId\": \"44ce10f1-325a-4c75-b026-b68c59d86971\"}}"; + SitecoreLayoutResponseContent resultContent = new(); + LayoutQueryResponse layoutQueryResponse = new() + { + Layout = new LayoutModel + { + Item = new ItemModel + { + Rendered = JsonSerializer.SerializeToElement(json) + } + } + }; + GraphQLResponse response = new() + { + Data = layoutQueryResponse + }; + + SitecoreLayoutRequest request = []; + request + .SiteName(name) + .ApiKey(apiKey); + + request.Add("sc_lang", language); + + _client.SendQueryAsync(Arg.Any()).Returns(response); + string? data = response.Data.Layout?.Item?.Rendered.ToString(); + + _serializer.Deserialize(data ?? string.Empty).Returns(resultContent); + + // Act + await _graphQlLayoutServiceHandler.Request(request, handlerName); + + // Assert + _serializer.Received().Deserialize(data ?? string.Empty); + } + + [Theory] + [AutoData] + public async Task Request_Should_ReturnResponse(string name, string apiKey, string language, string handlerName) + { + // Arrange + string json = " {\"sitecore\": {\"itemId\": \"44ce10f1-325a-4c75-b026-b68c59d86971\"}}"; + SitecoreLayoutResponseContent resultContent = new(); + LayoutQueryResponse layoutQueryResponse = new() + { + Layout = new LayoutModel + { + Item = new ItemModel + { + Rendered = JsonSerializer.SerializeToElement(json) + } + } + }; + GraphQLResponse response = new() + { + Data = layoutQueryResponse + }; + + SitecoreLayoutRequest request = []; + request + .SiteName(name) + .ApiKey(apiKey); + + request.Add("sc_lang", language); + + _client.SendQueryAsync(Arg.Any()).Returns(response); + string? data = response.Data.Layout?.Item?.Rendered.ToString(); + _serializer.Deserialize(data ?? string.Empty).Returns(resultContent); + + // Act + SitecoreLayoutResponse result = await _graphQlLayoutServiceHandler.Request(request, handlerName); + + // Assert + result.Content.Should().Be(resultContent); + } + + [Theory] + [InlineAutoData("en")] + [InlineAutoData(null)] + public async Task Request_Should_ReturnItemNotFoundSitecoreLayoutServiceClientException(string language, string name, string apiKey, string handlerName) + { + // Arrange + GraphQLResponse response = new(); + + SitecoreLayoutRequest request = []; + request + .SiteName(name) + .ApiKey(apiKey); + _client.SendQueryAsync(Arg.Any()).Returns(response); + + request.Add("sc_lang", language); + + // Act + SitecoreLayoutResponse result = await _graphQlLayoutServiceHandler.Request(request, handlerName); + + // Assert + result.Errors.Should().Contain(e => e is ItemNotFoundSitecoreLayoutServiceClientException); + result.Content.Should().BeNull(); + } + + [Theory] + [AutoData] + public async Task Request_Should_SitecoreLayoutServiceClientException(string name, string apiKey, string language, string handlerName) + { + // Arrange + const string json = " {\"sitecore\": {\"itemId\": \"44ce10f1-325a-4c75-b026-b68c59d86971\"}}"; + SitecoreLayoutResponseContent resultContent = new(); + LayoutQueryResponse layoutQueryResponse = new() + { + Layout = new LayoutModel + { + Item = new ItemModel + { + Rendered = JsonSerializer.SerializeToElement(json) + } + } + }; + GraphQLResponse response = new() + { + Errors = + [ + new GraphQLError() + ], + Data = layoutQueryResponse + }; + SitecoreLayoutRequest request = []; + request + .SiteName(name) + .ApiKey(apiKey); + + request.Add("sc_lang", language); + + _client.SendQueryAsync(Arg.Any()).Returns(response); + _serializer.Deserialize(Arg.Any()).Returns(resultContent); + + // Act + SitecoreLayoutResponse result = await _graphQlLayoutServiceHandler.Request(request, handlerName); + + // Assert + result.Errors.Should().Contain(e => e != null); + result.Content.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs new file mode 100644 index 0000000..acf6c5a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs @@ -0,0 +1,443 @@ +using System.Diagnostics.CodeAnalysis; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.MockModels; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Request.Handlers; + +public class HttpLayoutRequestHandlerFixture +{ + private const string StatusCodeKey = "HTTP Status Code"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Behaviors.Add(new OmitOnRecursionBehavior()); + + HttpClient client = new() + { + DefaultRequestVersion = new Version(1, 0), + BaseAddress = new Uri("http://localhost.test") + }; + + f.Inject(client); + + ISitecoreLayoutSerializer? serializer = f.Freeze(); + serializer.Deserialize("JSON").Returns(CannedResponses.Simple); + + f.Freeze(); + + f.Inject(new SitecoreLayoutRequest()); + }; + + public static Action HttpClientWithMockedHttpMessageHandler => f => + { + HttpMessageHandlerWrapper mockHttpHandler = new(); + + HttpClient client = new(mockHttpHandler) + { + DefaultRequestVersion = new Version(1, 0), + BaseAddress = new Uri("http://localhost.test") + }; + + f.Inject(client); + f.Inject(mockHttpHandler); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_HttpClientWithNullBaseAddress_Throws(HttpClient client, ISitecoreLayoutSerializer serializer, IOptionsSnapshot options, ILogger logger) + { + // Arrange + client.BaseAddress = null; + Func act = + () => new HttpLayoutRequestHandler(client, serializer, options, logger); + + // Act & assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithNullRequest_Throws(HttpLayoutRequestHandler sut) + { + // Arrange + Func> act = + () => sut.Request(null!, string.Empty); + + // Act & Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_WithEntriesMappedToHttpRequest_GenerateCorrectHttpRequestMessage( + HttpClient client, + ISitecoreLayoutSerializer serializer, + IOptionsSnapshot options, + SitecoreLayoutRequest request, + string handlerName, + ILogger logger) + { + // Arrange + request.Add("alpha", "this"); + request.Add("beta", "that"); + + const string hostHeader = "testhost"; + StringContent messageContent = new("test"); + HttpMethod messageMethod = HttpMethod.Post; + const string authCookie = "auth=test"; + + HttpLayoutRequestHandlerOptions handlerOptions = new() + { + RequestMap = + [ + (req, message) => + { + message.RequestUri = req.BuildUri(client.BaseAddress!, ["alpha", "beta"]); + message.Headers.Add("Host", hostHeader); + message.Headers.Add("Cookie", authCookie); + message.Method = messageMethod; + message.Content = messageContent; + } + + ] + }; + options.Get(handlerName).Returns(handlerOptions); + + HttpLayoutRequestHandlerWrapper stub = new(client, serializer, options, logger); + + // Act + await stub.Request(request, handlerName); + stub.RequestMessage!.Headers.TryGetValues("Cookie", out IEnumerable? cookies); + + // Assert + stub.RequestMessage!.RequestUri!.Query.Should().Contain("alpha=this&beta=that"); + stub.RequestMessage.Headers.Should().NotBeEmpty(); + stub.RequestMessage.Headers.Host.Should().Be(hostHeader); + cookies.Should().Contain(authCookie); + stub.RequestMessage.Method.Should().Be(messageMethod); + stub.RequestMessage.Content.Should().Be(messageContent); + } + + [Theory] + [InlineAutoNSubstituteData(""; + + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + HttpClient client = _server.CreateClient(); + + // Act + await client.GetAsync(MiddlewareController + "/" + QueryStringTestActionMethod + "?" + testQueryString); + + // Assert + _mockClientHandler.Requests.Single().RequestUri!.AbsoluteUri.Should() + .BeEquivalentTo($"{_layoutServiceUri.AbsoluteUri}?item=%2f{MiddlewareController}%2f{QueryStringTestActionMethod}¶m1=a+b¶m2=%3cscript+type%3d%22text%2fjavascript%22%3ealert(%22hello%22)%3b%3c%2fscript%3e"); + } + + [Fact] + public async Task HttpRequest_WithAuthenticationHeaders_HeadersMappedToLayoutServiceRequest() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }); + + HttpClient client = _server.CreateClient(); + + // Act + await client.GetAsync(MiddlewareController + "/" + QueryStringTestActionMethod) + ; + + // Assert + _mockClientHandler.Requests.Single().Headers.Authorization!.Scheme.Should().Be("Bearer"); + _mockClientHandler.Requests.Single().Headers.Authorization!.Parameter.Should().Be("TestToken"); + } + + [Fact] + public async Task HttpRequest_WithCookie_CookieMappedToLayoutServiceRequest() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }); + + HttpClient client = _server.CreateClient(); + + // Act + await client.GetAsync(MiddlewareController + "/" + QueryStringTestActionMethod) + ; + _mockClientHandler.Requests.Single().Headers.TryGetValues("Cookie", out IEnumerable? cookies); + + // Assert + cookies.Should().NotBeNull().And.Contain(TestCookie); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs new file mode 100644 index 0000000..bdca6d7 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs @@ -0,0 +1,95 @@ +using System.Net; +using FluentAssertions; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.AspNetCore.TestHost; +using NSubstitute; +using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Mocks; +using Sitecore.AspNetCore.SDK.SearchOptimization.Extensions; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; +using Sitecore.AspNetCore.SDK.SearchOptimization.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.SearchOptimization; + +public class EdgeSitemapProxyFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler = new(); + private readonly ISitemapService _mockSitemapService = Substitute.For(); + private readonly Uri _edgeSitemapUrl = new("https://xmcloud-test.com/sitemap.xml"); + + public EdgeSitemapProxyFixture() + { + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + TestServerBuilder testHostBuilder = new(); + _ = testHostBuilder + .ConfigureServices(builder => + { + builder.AddSingleton(_mockSitemapService); + builder.AddSingleton(_ => + { + return new CustomHttpClientFactory( + () => + new HttpClient(_mockClientHandler)); + }); + + IGraphQLClient? mockedGraphQlClient = Substitute.For(); + mockedGraphQlClient.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse + { + Data = new SiteInfoResultModel + { + Site = new Site + { + SiteInfo = new SiteInfo + { + Sitemap = + [ + _edgeSitemapUrl.ToString() + ] + } + } + } + }); + + builder.AddSingleton(mockedGraphQlClient); + builder.AddEdgeSitemap(); + }) + .Configure(app => + { + app.UseSitemap(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task EdgeSitemap_MustBeProxied() + { + // Arrange + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/sitemap.xml", UriKind.Relative)); + _mockSitemapService.GetSitemapUrl(Arg.Any(), Arg.Any()) + .Returns(_edgeSitemapUrl.AbsoluteUri); + + // Act + await client.SendAsync(request); + + // Asserts + _mockClientHandler.Requests.Should().ContainSingle(); + _mockClientHandler.Requests[0].RequestUri!.Host.Should().Be(_edgeSitemapUrl.Host); + _mockClientHandler.Requests[0].RequestUri!.Scheme.Should().Be(_edgeSitemapUrl.Scheme); + _mockClientHandler.Requests[0].RequestUri!.PathAndQuery.Should().Be("/sitemap.xml"); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitecoreRewriteFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitecoreRewriteFixture.cs new file mode 100644 index 0000000..400cb91 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitecoreRewriteFixture.cs @@ -0,0 +1,273 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Caching.Memory; +using NSubstitute; +using Sitecore.AspNetCore.SDK.SearchOptimization.Extensions; +using Sitecore.AspNetCore.SDK.SearchOptimization.Models; +using Sitecore.AspNetCore.SDK.SearchOptimization.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.SearchOptimization; + +public class SitecoreRewriteFixture +{ + [Fact] + public async Task CheckRewritePath_MultipleRulesWithSkipRemaining() + { + using IHost host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseSitecoreRedirects(); + app.Run(context => context.Response.WriteAsync( + context.Request.Scheme + + "://" + + context.Request.Host + + context.Request.Path + + context.Request.QueryString)); + }); + }).ConfigureServices(services => + { + services.AddSingleton(_ => + { + IRedirectsService? mockedRedirectService = Substitute.For(); + mockedRedirectService.GetRedirects(Arg.Any()).Returns([ + new RedirectInfo + { + Pattern = "(.*)", Target = "http://example.com/$1", RedirectType = RedirectType.SERVER_TRANSFER, IsQueryStringPreserved = true + }, + new RedirectInfo + { + Pattern = "(.*)", Target = "http://example.com/42", RedirectType = RedirectType.SERVER_TRANSFER, IsQueryStringPreserved = true + } + ]); + + return mockedRedirectService; + }); + + services.AddSingleton(_ => + { + IMemoryCache? mockedMemoryCache = Substitute.For(); + return mockedMemoryCache; + }); + }).Build(); + + await host.StartAsync(); + + TestServer server = host.GetTestServer(); + + string response = await server.CreateClient().GetStringAsync("foo"); + + response.Should().Be("http://example.com/foo"); + } + + [Fact] + public async Task CheckIfEmptyStringRedirectCorrectly() + { + using IHost host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseSitecoreRedirects(); + }); + }).ConfigureServices(services => + { + services.AddSingleton(_ => + { + IRedirectsService? mockedRedirectService = Substitute.For(); + mockedRedirectService.GetRedirects(Arg.Any()).Returns([ + new RedirectInfo + { + Pattern = "(.*)", Target = "$1", RedirectType = RedirectType.REDIRECT_301 + } + ]); + + return mockedRedirectService; + }); + + services.AddSingleton(_ => + { + IMemoryCache? mockedMemoryCache = Substitute.For(); + return mockedMemoryCache; + }); + }).Build(); + + await host.StartAsync(); + + TestServer server = host.GetTestServer(); + + HttpResponseMessage response = await server.CreateClient().GetAsync(string.Empty); + response.Headers.Location!.OriginalString.Should().Be("/"); + } + + [Fact] + public async Task CheckIfEmptyStringRewriteCorrectly() + { + using IHost host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseRewriter(); + app.Run(context => context.Response.WriteAsync( + context.Request.Path + + context.Request.QueryString)); + }); + }).ConfigureServices(services => + { + services.AddSingleton(_ => + { + IRedirectsService? mockedRedirectService = Substitute.For(); + mockedRedirectService.GetRedirects(Arg.Any()).Returns([ + new RedirectInfo + { + Pattern = "(.*)", Target = "$1", RedirectType = RedirectType.SERVER_TRANSFER + } + ]); + + return mockedRedirectService; + }); + }).Build(); + + await host.StartAsync(); + + TestServer server = host.GetTestServer(); + + string response = await server.CreateClient().GetStringAsync(string.Empty); + + response.Should().Be("/"); + } + + [Theory] + [InlineData("(.*)", "http://example.com/$1", true, "http://example.com", "path", "http://example.com/path")] + [InlineData("(.*)", "http://example.com", true, "http://example.com/", "", "http://example.com/")] + [InlineData("path/(.*)", "path?value=$1", true, null, "path/value", "/path?value=value")] + [InlineData("path/(.*)", "path?param=$1", true, null, "path/value?param1=OtherValue", "/path?param1=OtherValue¶m=value")] + [InlineData("path/(.*)", "http://example.com/pathBase/path?param=$1", true, "http://example.com", "path/value?param1=OtherValue", "http://example.com/pathBase/path?param=value¶m1=OtherValue")] + [InlineData("^/ab[cd]/$", "graphql", true, null, "abc", "/graphql")] + [InlineData("/bro/", "graphql", true, null, "bro", "/graphql")] + [InlineData("/bro/(.*)", "graphql", true, null, "bro/add", "/graphql")] + [InlineData("bro/(.*)", "graphql", true, null, "bro/add", "/graphql")] + [InlineData("/bro", "graphql", false, null, "bro?sc_test=1", "/graphql")] + [InlineData("/bro", "graphql", true, null, "bro?sc_test=1", "/graphql?sc_test=1")] + [InlineData("/bro", "graphql?sc_test2=2", true, null, "bro?sc_test=1", "/graphql?sc_test=1&sc_test2=2")] + [InlineData("/bro", "graphql?sc_test2=2", false, null, "bro?sc_test=1", "/graphql?sc_test2=2")] + internal async Task CheckRewritePath(string pattern, string replacement, bool isQueryStringPreserved, string? baseAddress, string requestUrl, string expectedUrl) + { + using IHost host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseSitecoreRedirects(); + app.Run(context => context.Response.WriteAsync( + baseAddress! + + context.Request.Path + + context.Request.QueryString)); + }); + }).ConfigureServices(services => + { + services.AddSingleton(_ => + { + IRedirectsService? mockedRedirectService = Substitute.For(); + mockedRedirectService.GetRedirects(Arg.Any()).Returns([ + new RedirectInfo + { + Pattern = pattern, Target = replacement, RedirectType = RedirectType.SERVER_TRANSFER, IsQueryStringPreserved = isQueryStringPreserved + } + ]); + + return mockedRedirectService; + }); + + services.AddSingleton(_ => + { + IMemoryCache? mockedMemoryCache = Substitute.For(); + return mockedMemoryCache; + }); + }).Build(); + + await host.StartAsync(); + + TestServer server = host.GetTestServer(); + + string response = await server.CreateClient().GetStringAsync(requestUrl); + + response.Should().Be(expectedUrl); + } + + [Theory] + [InlineData("(.*)", "http://example.com/$1", RedirectType.REDIRECT_301, true, null, "path", "http://example.com/path")] + [InlineData("(.*)", "http://example.com", RedirectType.REDIRECT_301, true, null, "", "http://example.com/")] + [InlineData("path/(.*)", "path?value=$1", RedirectType.REDIRECT_301, true, null, "path/value", "/path?value=value")] + [InlineData("path/(.*)", "path?param=$1", RedirectType.REDIRECT_301, true, null, "path/value?param1=OtherValue", "/path?param1=OtherValue¶m=value")] + [InlineData("path/(.*)", "http://example.com/pathBase/path?param=$1", RedirectType.REDIRECT_301, true, "http://example.com/pathBase", "path/value?param1=OtherValue", "http://example.com/pathBase/path?param1=OtherValue¶m=value")] + [InlineData("path/(.*)", "http://hoψst.com/pÂthBase/path?parãm=$1", RedirectType.REDIRECT_301, true, "http://example.com/pathBase", "path/value?päram1=OtherValüe", "http://xn--host-cpd.com/p%C3%82thBase/path?p%C3%A4ram1=OtherVal%C3%BCe&parãm=value")] + [InlineData("(.*)", "http://example.com/$1", RedirectType.REDIRECT_302, true, null, "path", "http://example.com/path")] + [InlineData("^/ab[cd]/$", "graphql", RedirectType.REDIRECT_301, true, null, "abc", "/graphql")] + [InlineData("/bro/", "graphql", RedirectType.REDIRECT_301, true, null, "bro", "/graphql")] + [InlineData("/bro/(.*)", "graphql", RedirectType.REDIRECT_301, true, null, "bro/add", "/graphql")] + [InlineData("bro/(.*)", "graphql", RedirectType.REDIRECT_301, true, null, "bro/add", "/graphql")] + [InlineData("/bro", "graphql", RedirectType.REDIRECT_301, false, null, "bro?sc_test=1", "/graphql")] + [InlineData("/bro", "graphql", RedirectType.REDIRECT_301, true, null, "bro?sc_test=1", "/graphql?sc_test=1")] + [InlineData("/bro", "graphql?sc_test2=2", RedirectType.REDIRECT_301, true, null, "bro?sc_test=1", "/graphql?sc_test=1&sc_test2=2")] + [InlineData("/bro", "graphql?sc_test2=2", RedirectType.REDIRECT_301, false, null, "bro?sc_test=1", "/graphql?sc_test2=2")] + internal async Task CheckRedirectPath(string pattern, string replacement, RedirectType redirectType, bool isQueryStringPreserved, string? baseAddress, string requestUrl, string expectedUrl) + { + using IHost host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseSitecoreRedirects(); + }); + }).ConfigureServices(services => + { + services.AddSingleton(_ => + { + IRedirectsService? mockedRedirectService = Substitute.For(); + mockedRedirectService.GetRedirects(Arg.Any()).Returns([ + new RedirectInfo + { + Pattern = pattern, Target = replacement, RedirectType = redirectType, IsQueryStringPreserved = isQueryStringPreserved + } + ]); + + return mockedRedirectService; + }); + + services.AddSingleton(_ => + { + IMemoryCache? mockedMemoryCache = Substitute.For(); + return mockedMemoryCache; + }); + }).Build(); + + await host.StartAsync(); + + TestServer server = host.GetTestServer(); + if (!string.IsNullOrEmpty(baseAddress)) + { + server.BaseAddress = new Uri(baseAddress); + } + + HttpResponseMessage response = await server.CreateClient().GetAsync(requestUrl); + + HttpStatusCode expectedRedirectCode = redirectType == RedirectType.REDIRECT_301 ? HttpStatusCode.MovedPermanently : HttpStatusCode.Redirect; + response.StatusCode.Should().Be(expectedRedirectCode); + response.Headers.Location!.OriginalString.Should().Be(expectedUrl); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitemapProxyFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitemapProxyFixture.cs new file mode 100644 index 0000000..44709e6 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/SitemapProxyFixture.cs @@ -0,0 +1,67 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Mocks; +using Sitecore.AspNetCore.SDK.SearchOptimization.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.SearchOptimization; + +public class SitemapProxyFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler = new(); + private readonly Uri _cdInstanceUri = new("http://cd"); + + public SitemapProxyFixture() + { + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + TestServerBuilder testHostBuilder = new(); + _ = testHostBuilder + .ConfigureServices(builder => + { + builder.AddSingleton(_ => + { + return new CustomHttpClientFactory( + () => + new HttpClient(_mockClientHandler)); + }); + + builder.AddSitemap(c => c.Url = _cdInstanceUri); + }) + .Configure(app => + { + app.UseSitemap(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task SitemapRequest_MustBeProxied() + { + // Arrange + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/sitemap.xml", UriKind.Relative)); + + // Act + await client.SendAsync(request); + + // Asserts + _mockClientHandler.Requests.Should().ContainSingle(); + _mockClientHandler.Requests[0].RequestUri!.Host.Should().Be(_cdInstanceUri.Host); + _mockClientHandler.Requests[0].RequestUri!.Scheme.Should().Be(_cdInstanceUri.Scheme); + _mockClientHandler.Requests[0].RequestUri!.PathAndQuery.Should().Be("/sitemap.xml"); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SitecoreLayoutClientBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SitecoreLayoutClientBuilderExtensionsFixture.cs new file mode 100644 index 0000000..93401ed --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SitecoreLayoutClientBuilderExtensionsFixture.cs @@ -0,0 +1,56 @@ +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Configuration; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures; + +public class SitecoreLayoutClientBuilderExtensionsFixture : IDisposable +{ + private readonly HttpLayoutClientMessageHandler _messageHandler; + private readonly TestServer _server; + + public SitecoreLayoutClientBuilderExtensionsFixture() + { + TestServerBuilder testHostBuilder = new(); + _messageHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + ISitecoreLayoutClientBuilder lsc = builder + .AddSitecoreLayoutService(); + + lsc.AddHttpHandler("mock", _ => new HttpClient(_messageHandler) { BaseAddress = new Uri("http://layout.service") }); + + lsc.AddHttpHandler("otherMock", _ => new HttpClient(_messageHandler) { BaseAddress = new Uri("http://layout.service") }) + .AsDefaultHandler(); + }) + .Configure(app => + { + app.UseSitecoreRenderingEngine(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public void DefaultHandler_SetsSitecoreLayoutServiceOptions() + { + // Act + IOptions layoutService = _server.Services.GetRequiredService>(); + + // Assert + layoutService.Value.DefaultHandler.Should().Be("otherMock"); + } + + public void Dispose() + { + _messageHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/AllFieldTagHelpersFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/AllFieldTagHelpersFixture.cs new file mode 100644 index 0000000..31d823c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/AllFieldTagHelpersFixture.cs @@ -0,0 +1,97 @@ +using System.Globalization; +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class AllFieldTagHelpersFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public AllFieldTagHelpersFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-All-Field-Types", "ComponentWithAllFieldTypes") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task ComponentWithAllFieldTypes_RendersFieldsCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-allfields")); + + // Assert + sectionNode.ChildNodes.First(n => n.Name.Equals("h1", StringComparison.OrdinalIgnoreCase)).InnerText.Should().Be(TestConstants.TestFieldValue); + + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div1", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.TestMultilineFieldValue.Replace(Environment.NewLine, "
", StringComparison.OrdinalIgnoreCase)); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div2", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.RichTextFieldValue1); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div3", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.RichTextFieldValue2); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div4", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.LinkFieldValue); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div5", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.AllFieldsImageValue); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div6", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.DateFieldValue); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div7", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.MediaLibraryItemImageFieldValue); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div8", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(9.99m.ToString(CultureInfo.CurrentCulture)); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/DateFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/DateFieldTagHelperFixture.cs new file mode 100644 index 0000000..707c6c8 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/DateFieldTagHelperFixture.cs @@ -0,0 +1,107 @@ +using System.Globalization; +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class DateFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public DateFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-Dates", "ComponentWithDates") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task DateTagHelper_DoesNotResetOtherTagHelperOutput() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-dates")); + HtmlNode? text = sectionNode.ChildNodes[7]; + + // Assert + // check scenario that DateTagHelper does not reset values of nested helpers. + text.InnerHtml.Should().Contain(TestConstants.TestFieldValue); + } + + [Fact] + public async Task DateTagHelper_GeneratesProperDate() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-dates")); + + // Assert + sectionNode.ChildNodes[1].InnerHtml.Should().Be("05/04/2012"); + sectionNode.ChildNodes[3].InnerHtml.Should().Be("05/04/2012 00:00:00"); + sectionNode.ChildNodes[5].InnerHtml.Should().Be(TestConstants.DateTimeValue.ToString(CultureInfo.CurrentCulture)); + sectionNode.ChildNodes[9].InnerHtml.Should().Contain("04.05.2012"); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/FileFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/FileFieldTagHelperFixture.cs new file mode 100644 index 0000000..f34b635 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/FileFieldTagHelperFixture.cs @@ -0,0 +1,245 @@ +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class FileFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public FileFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-Files", "ComponentWithFiles") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task FileTagHelper_RendersAttributeFromModel() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-files")); + HtmlNode? downloadLink1 = sectionNode.ChildNodes[1]; + HtmlNode? downloadLink2 = sectionNode.ChildNodes[3]; + HtmlNode? downloadLink3 = sectionNode.ChildNodes[5]; + HtmlNode? downloadLink4 = sectionNode.ChildNodes[7]; + + // Assert + downloadLink1.InnerHtml.Should().Contain("Download link text"); + downloadLink3.Attributes.Should().HaveCount(3); + downloadLink1.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink1.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink1.Attributes["href"].Value.Should().Be("/doc.pdf"); + + downloadLink2.InnerHtml.Should().Contain("Download link text"); + downloadLink3.Attributes.Should().HaveCount(3); + downloadLink2.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink2.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink2.Attributes["href"].Value.Should().Be("/doc.pdf"); + + // Assert + downloadLink3.InnerHtml.Should().Contain("Download link text"); + downloadLink3.Attributes.Should().HaveCount(3); + downloadLink3.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink3.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink3.Attributes["href"].Value.Should().Be("/doc.pdf"); + + downloadLink4.InnerHtml.Should().Contain("Download link text"); + downloadLink3.Attributes.Should().HaveCount(3); + downloadLink4.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink4.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink4.Attributes["href"].Value.Should().Be("/doc.pdf"); + } + + [Fact] + public async Task FileTagHelper_RendersCustomTagsAttributes() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-files")); + HtmlNode? downloadLink1 = sectionNode.ChildNodes[9]; + HtmlNode? downloadLink2 = sectionNode.ChildNodes[11]; + HtmlNode? downloadLink3 = sectionNode.ChildNodes[13]; + HtmlNode? downloadLink4 = sectionNode.ChildNodes[15]; + + // Assert + downloadLink1.InnerHtml.Should().Contain("Download link text"); + downloadLink1.Attributes.Should().HaveCount(6); + downloadLink1.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink1.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink1.Attributes["href"].Value.Should().Be("/doc.pdf"); + downloadLink1.Attributes["class"].Value.Should().Be("test-class"); + downloadLink1.Attributes["target"].Value.Should().Be("_blank"); + downloadLink1.Attributes["custom-attribute"].Value.Should().Be("test-custom-attribute"); + + downloadLink2.InnerHtml.Should().Contain("Download link text"); + downloadLink2.Attributes.Should().HaveCount(6); + downloadLink2.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink2.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink2.Attributes["href"].Value.Should().Be("/doc.pdf"); + downloadLink2.Attributes["class"].Value.Should().Be("test-class"); + downloadLink2.Attributes["target"].Value.Should().Be("_blank"); + downloadLink2.Attributes["custom-attribute"].Value.Should().Be("test-custom-attribute"); + + downloadLink3.InnerHtml.Should().Contain("Download link text"); + downloadLink3.Attributes.Should().HaveCount(6); + downloadLink3.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink3.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink3.Attributes["href"].Value.Should().Be("/doc.pdf"); + downloadLink3.Attributes["class"].Value.Should().Be("test-class"); + downloadLink3.Attributes["target"].Value.Should().Be("_blank"); + downloadLink3.Attributes["custom-attribute"].Value.Should().Be("test-custom-attribute"); + + downloadLink4.InnerHtml.Should().Contain("Download link text"); + downloadLink4.Attributes.Should().HaveCount(6); + downloadLink4.Attributes["type"].Value.Should().Be("application/pdf"); + downloadLink4.Attributes["title"].Value.Should().Be("Download link description"); + downloadLink4.Attributes["href"].Value.Should().Be("/doc.pdf"); + downloadLink4.Attributes["class"].Value.Should().Be("test-class"); + downloadLink4.Attributes["target"].Value.Should().Be("_blank"); + downloadLink4.Attributes["custom-attribute"].Value.Should().Be("test-custom-attribute"); + } + + [Fact] + public async Task FileTagHelper_OverridesModelAttributes() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-files")); + HtmlNode? downloadLink1 = sectionNode.ChildNodes[17]; + HtmlNode? downloadLink2 = sectionNode.ChildNodes[19]; + HtmlNode? downloadLink3 = sectionNode.ChildNodes[21]; + HtmlNode? downloadLink4 = sectionNode.ChildNodes[23]; + + // Assert + downloadLink1.InnerHtml.Should().Contain("Custom Download Link"); + downloadLink1.Attributes.Should().HaveCount(3); + downloadLink1.Attributes["type"].Value.Should().Be("application/custom"); + downloadLink1.Attributes["title"].Value.Should().Be("custom-title"); + downloadLink1.Attributes["href"].Value.Should().Be("/customLink"); + + downloadLink2.InnerHtml.Should().Contain("Custom Download Link"); + downloadLink2.Attributes.Should().HaveCount(3); + downloadLink2.Attributes["type"].Value.Should().Be("application/custom"); + downloadLink2.Attributes["title"].Value.Should().Be("custom-title"); + downloadLink2.Attributes["href"].Value.Should().Be("/customLink"); + + downloadLink3.InnerHtml.Should().Contain("Custom Download Link"); + downloadLink3.Attributes.Should().HaveCount(3); + downloadLink3.Attributes["type"].Value.Should().Be("application/custom"); + downloadLink3.Attributes["title"].Value.Should().Be("custom-title"); + downloadLink3.Attributes["href"].Value.Should().Be("/customLink"); + + downloadLink4.InnerHtml.Should().Contain("Custom Download Link"); + downloadLink4.Attributes.Should().HaveCount(3); + downloadLink4.Attributes["type"].Value.Should().Be("application/custom"); + downloadLink4.Attributes["title"].Value.Should().Be("custom-title"); + downloadLink4.Attributes["href"].Value.Should().Be("/customLink"); + } + + [Fact] + public async Task FileTagHelper_RendersInnerHtml() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-files")); + HtmlNode? downloadLink1 = sectionNode.ChildNodes[25]; + HtmlNode? downloadLink2 = sectionNode.ChildNodes[27]; + HtmlNode? downloadLink3 = sectionNode.ChildNodes[29]; + HtmlNode? downloadLink4 = sectionNode.ChildNodes[31]; + + // Assert + downloadLink1.InnerHtml.Should().Contain("

Inner html

"); + downloadLink2.InnerHtml.Should().Contain("

Inner html

"); + downloadLink3.InnerHtml.Should().Contain("

Inner html

"); + downloadLink4.InnerHtml.Should().Contain("

Inner html

"); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/ImageFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/ImageFieldTagHelperFixture.cs new file mode 100644 index 0000000..bde8dc1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/ImageFieldTagHelperFixture.cs @@ -0,0 +1,184 @@ +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class ImageFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public ImageFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-Images", "ComponentWithImages") + .AddViewComponent("Component-1", "Component1") + .AddModelBoundView("Component-2", "Component2") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task ImgTagHelper_GeneratedProperImageWithCustomAttributes() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.PageWithPreview)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-images")); + + // Assert + // check scenario that ImageTagHelper render proper image tag with custom attributes. + sectionNode.ChildNodes[5].OuterHtml.Should().Contain(TestConstants.SecondImageTestValue); + } + + [Fact] + public async Task ImgTagHelper_GeneratesImageTags() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.PageWithPreview)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-images")); + + // Assert + // check that there is proper number of 'img' tags generated. + sectionNode.ChildNodes.Count(n => n.Name.Equals("img", StringComparison.OrdinalIgnoreCase)).Should().Be(2); + } + + [Fact] + public async Task ImgTagHelper_GeneratedProperHtmlWithoutTagName() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.PageWithPreview)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-images")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes[1].OuterHtml.Should().Contain(TestConstants.ImageFieldValue); + } + + [Fact] + public async Task ImgTagHelper_GeneratesProperImageUrlIncludingImageParams() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.PageWithPreview)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-images")); + HtmlNode? lastImage = sectionNode.ChildNodes.Last(n => n.Name.Equals("img", StringComparison.OrdinalIgnoreCase)); + + // Assert + // check that image url contains mw and mh parameters + lastImage.Attributes.Should().Contain(a => a.Name == "src"); + lastImage.Attributes["src"].Value.Should().Contain("mw=100&mh=50"); + } + + [Fact] + public async Task ImgTagHelper_GeneratesProperEditableImageMarkupWithCustomProperties() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.EditablePage)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-1")).ChildNodes.First(n => n.HasClass("component-2")); + + // Assert + // check that editable markup contains all custom params + sectionNode.InnerHtml.Should().Contain("height=\"50\""); + sectionNode.InnerHtml.Should().Contain("width=\"94\""); + sectionNode.InnerHtml.Should().Contain("class=\"image1\""); + sectionNode.InnerHtml.Should().Contain("alt=\"customAlt\""); + sectionNode.InnerHtml.Should().Contain("src=\"/sitecore/shell/-/jssmedia/styleguide/data/media/img/sc_logo.png?mw=100&mh=50\""); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/LinkFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/LinkFieldTagHelperFixture.cs new file mode 100644 index 0000000..83cb17e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/LinkFieldTagHelperFixture.cs @@ -0,0 +1,282 @@ +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class LinkFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public LinkFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-Links", "ComponentWithLinks") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task LinkTagHelper_DoesNotResetOtherTagHelperOutput() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + HtmlNode? secondLink = sectionNode.ChildNodes[3]; + + // Assert + // check scenario that LinkTagHelper does not reset values of nested helpers. + secondLink.InnerHtml.Should().Contain(TestConstants.TestFieldValue); + } + + [Fact] + public async Task LinkTagHelper_GeneratesAnchorTags() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that there is proper number of 'a' tags generated. + sectionNode.ChildNodes.Count(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase)).Should().Be(7); + } + + [Fact] + public async Task LinkTagHelper_PrioritizeUserProvidedLinkText() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.First(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase)).InnerText.Should().Contain("Sample internal link"); + } + + [Fact] + public async Task LinkTagHelper_RenderFieldLinkTextIfNoInnerContent() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.First(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase) && n.HasClass("author-text")).InnerText.Should().Contain("This is field text"); + } + + [Fact] + public async Task LinkTagHelper_RendersLinkAttributes() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + HtmlNode? linkNode = sectionNode.ChildNodes.Last(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase)); + + linkNode.Should().NotBeNull(); + linkNode.Attributes.SingleOrDefault(a => a.Name == "href").Should().NotBeNull(); + linkNode.Attributes.Single(a => a.Name == "href").Value.Should().NotBe(string.Empty); + linkNode.Attributes.SingleOrDefault(a => a.Name == "target").Should().NotBeNull(); + linkNode.Attributes.Single(a => a.Name == "target").Value.Should().NotBe(string.Empty); + linkNode.Attributes.SingleOrDefault(a => a.Name == "title").Should().NotBeNull(); + linkNode.Attributes.Single(a => a.Name == "title").Value.Should().NotBe(string.Empty); + linkNode.Attributes.SingleOrDefault(a => a.Name == "class").Should().NotBeNull(); + linkNode.Attributes.Single(a => a.Name == "class").Value.Should().NotBe(string.Empty); + } + + [Fact] + public async Task LinkTagHelper_GeneratesNestedTags() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.Last(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase)).InnerText.Should().Be(TestConstants.TestFieldValue); + } + + [Fact] + public async Task LinkTagHelper_DoesNotTrimUserAttributes() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.Last(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase)).Attributes.Contains("data-test"); + } + + [Fact] + public async Task LinkTagHelper_RenderFieldAuthorLinkTextInEEIfEditableTrue() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.First(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase) && n.HasClass("author-text-ee")).InnerText.Should().Contain("This is field text"); + } + + [Fact] + public async Task LinkTagHelper_RenderFieldCustomLinkTextInEEIfEditableFalse() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-links")); + + // Assert + // check that link will contain user provided link text. + sectionNode.ChildNodes.First(n => n.Name.Equals("a", StringComparison.OrdinalIgnoreCase) && n.HasClass("author-text-ee-editable-false")).InnerText.Should().Contain("custom text"); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/NumberFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/NumberFieldTagHelperFixture.cs new file mode 100644 index 0000000..2c2849b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/NumberFieldTagHelperFixture.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using System.Net; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class NumberFieldTagHelperFixture : IDisposable +{ + private const decimal TestValue = 1.21M; + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public NumberFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-With-Number", "ComponentWithNumber") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task NumberTagHelper_DoesNotResetOtherTagHelperOutput() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-number")); + HtmlNode? text = sectionNode.ChildNodes[3]; + + // Assert + // check scenario that NumberTagHelper does not reset values of nested helpers. + text.InnerHtml.Should().Contain(TestConstants.TestFieldValue); + } + + [Fact] + public async Task NumberHelper_GeneratesProperNumber() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-with-number")); + + // Assert + sectionNode.ChildNodes[1].ChildNodes[1].InnerHtml.Should().Contain(TestValue.ToString("C", CultureInfo.CurrentCulture)); + sectionNode.ChildNodes[1].ChildNodes[2].InnerHtml.Should().Contain(TestValue.ToString("C", CultureInfo.CreateSpecificCulture("ua-ua"))); + sectionNode.ChildNodes[1].ChildNodes[3].InnerHtml.Should().Contain(TestValue.ToString(CultureInfo.CurrentCulture)); + sectionNode.ChildNodes[1].ChildNodes[4].InnerHtml.Should().Contain(TestValue.ToString("P", CultureInfo.CurrentCulture)); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/PlaceholderTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/PlaceholderTagHelperFixture.cs new file mode 100644 index 0000000..4600a0f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/PlaceholderTagHelperFixture.cs @@ -0,0 +1,129 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class PlaceholderTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public PlaceholderTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddViewComponent("Component-1", "Component1") + .AddModelBoundView("Component-2", "Component2") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task PageInNormalMode_WithNestedPlaceholderComponent_ComponentIsRenderedCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + // Assert + response.Should().Contain(TestConstants.RichTextFieldValue1); + } + + [Fact] + public async Task PageInEditableMode_WithComponentsAndChromes_ComponentAndChromesAreRenderedCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.EditablePage)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + // Assert + response.Should().Contain(TestConstants.RichTextFieldValue1); + response.Should().Contain("{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}"); + response.Should().Contain(""); + response.Should().Contain(""); + response.Should().Contain(""); + response.Should().Contain(""); + } + + [Fact] + public async Task PageInHorizonEditableMode_WithComponentsAndChromes_ComponentAndChromesAreRenderedCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.HorizonEditablePage)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + // Assert + response.Should().Contain(TestConstants.RichTextFieldValue1); + response.Should().Contain("{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"jss-main\",\"placeholderMetadataKeys\":[\"jss-main\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"D8120D14950758B8BEF2E7A5158BD50F\",\"71AAF5C592425806BE7CECDF9154840B\",\"F351174D07C0547FBBDAEE51349C7DF5\",\"9B43C994B9835EC1A578401EA9478115\",\"FB33EEE82D6F5DD49CC71A5CBEBA728F\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{E02DDB9B-A062-5E50-924A-1940D7E053CE}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout\",\"jss-styleguide-layout\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"A7BFC343F487521593488FBB0D209FA6\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-layout\",\"expandedDisplayName\":null}"); + response.Should().Contain("{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{B7C779DA-2B75-586C-B40D-081FCB864256}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}"); + response.Should().Contain(""); + response.Should().Contain(""); + response.Should().Contain(""); + response.Should().Contain(""); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/RichTextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/RichTextFieldTagHelperFixture.cs new file mode 100644 index 0000000..1e248ae --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/RichTextFieldTagHelperFixture.cs @@ -0,0 +1,149 @@ +using System.Net; +using System.Text.Encodings.Web; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class RichTextFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public RichTextFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-4", "Component4") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task RichTextFieldTagHelper_DoesNotResetOtherTagHelperOutput() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-4")); + + // Assert + // check scenario that RichTextTagHelper does not reset values of another helpers. + sectionNode.ChildNodes.First(n => n.Name.Equals("textarea", StringComparison.OrdinalIgnoreCase)).InnerText.Should().Contain("12/12/2019"); + } + + [Fact] + public async Task RichTextFieldTagHelper_RendersFieldsCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-4")); + + // Assert + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div1", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.RichTextFieldValue1); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div2", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.RichTextFieldValue2); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div3", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().BeEmpty(); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div4", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().BeEmpty(); + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div5", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.TestFieldValue); + } + + [Fact] + public async Task RichTextFieldTagHelper_RendersEditableFieldsCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.HorizonEditablePage)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-4")); + + HtmlDocument expected = new(); + expected.LoadHtml("{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}" + + TestConstants.RichTextFieldValue1 + ""); + + // Assert + // RichTextField1 is editable + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div1", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(expected.DocumentNode.InnerHtml); + + // RichTextField2 is NOT editable + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase) && n.Id.Equals("div2", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.RichTextFieldValue2); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/TextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/TextFieldTagHelperFixture.cs new file mode 100644 index 0000000..bf0946e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/TagHelpers/TextFieldTagHelperFixture.cs @@ -0,0 +1,128 @@ +using System.Net; +using System.Text.Encodings.Web; +using FluentAssertions; +using HtmlAgilityPack; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.TagHelpers; + +public class TextFieldTagHelperFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + public TextFieldTagHelperFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + builder.AddSitecoreRenderingEngine(options => + { + options + .AddModelBoundView("Component-3", "Component3") + .AddDefaultComponentRenderer(); + }); + }) + .Configure(app => + { + app.UseRouting(); + app.UseSitecoreRenderingEngine(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task TextFieldTagHelper_RendersFieldsCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-3")); + + // Assert + sectionNode.ChildNodes.First(n => n.Name.Equals("h1", StringComparison.OrdinalIgnoreCase)).InnerText.Should().Be(TestConstants.TestFieldValue); + + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase)).InnerText + .Should().BeEmpty(); + + sectionNode.ChildNodes.First(n => n.Name.Equals("p", StringComparison.OrdinalIgnoreCase)).InnerText + .Should().BeEmpty(); + + sectionNode.ChildNodes.First(n => n.Name.Equals("textarea", StringComparison.OrdinalIgnoreCase)).InnerText + .Should().Be(HtmlEncoder.Default.Encode(TestConstants.RichTextFieldValue1)); + + sectionNode.ChildNodes.First(n => n.Name.Equals("span", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(TestConstants.TestMultilineFieldValue.Replace(Environment.NewLine, "
", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task TextFieldTagHelper_RendersEditableFieldsCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.HorizonEditablePage)) + }); + + HttpClient client = _server.CreateClient(); + + // Act + string response = await client.GetStringAsync(new Uri("/", UriKind.Relative)); + + HtmlDocument doc = new(); + + doc.LoadHtml(response); + HtmlNode? sectionNode = doc.DocumentNode.ChildNodes.First(n => n.HasClass("component-3")); + + HtmlDocument expected = new(); + expected.LoadHtml("{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"fieldId\":\"152f40ed-fe76-5861-b425-522375549742\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}" + + TestConstants.TestFieldValue + ""); + + // Assert + // TestField is editable + sectionNode.ChildNodes.First(n => n.Name.Equals("h1", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(expected.DocumentNode.InnerHtml); + + // EmptyField is NOT editable + sectionNode.ChildNodes.First(n => n.Name.Equals("div", StringComparison.OrdinalIgnoreCase)).InnerHtml + .Should().Be(string.Empty); + } + + public void Dispose() + { + _mockClientHandler.Dispose(); + _server.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/AttributeBasedTrackingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/AttributeBasedTrackingFixture.cs new file mode 100644 index 0000000..2606723 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/AttributeBasedTrackingFixture.cs @@ -0,0 +1,144 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; +using Sitecore.AspNetCore.SDK.Tracking; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Tracking; + +public class AttributeBasedTrackingFixture : IDisposable +{ + private static readonly string[] AspSessionId = + [ + "ASP.NET_SessionId=rku2oxmotbrkwkfxe0cpfrvn; path=/; HttpOnly; SameSite=Lax" + ]; + + private static readonly string[] AnalyticsCookie = + [ + "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + ]; + + private readonly TestServer _server; + + private readonly HttpLayoutClientMessageHandler _mockClientHandler; + + private readonly Uri _layoutServiceUri = new("http://layout.service"); + + private readonly Uri _cmInstanceUri = new("http://layout.service"); + + public AttributeBasedTrackingFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler = new HttpLayoutClientMessageHandler(); + + _ = testHostBuilder + .ConfigureServices(builder => + { + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) + { + BaseAddress = _layoutServiceUri + }) + .AsDefaultHandler(); + + builder.AddSitecoreRenderingEngine(options => + { + options + .AddDefaultComponentRenderer(); + }) + .WithTracking(); + + builder.AddSitecoreVisitorIdentification(o => o.SitecoreInstanceUri = _cmInstanceUri); + }) + .Configure(app => + { + app.UseSitecoreVisitorIdentification(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task SitecoreLayoutServiceResponseMetadata_ProxyCookies() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithVisitorIdentificationLayoutPlaceholder)), + Headers = + { + { "Set-Cookie", AspSessionId }, + { "Set-Cookie", AnalyticsCookie } + } + }); + + HttpClient client = _server.CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(new Uri("/AttributeBased", UriKind.Relative)); + + response.Headers.GetValues("Set-Cookie").Should().HaveCount(2); + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("ASP.NET_SessionId=", StringComparison.OrdinalIgnoreCase)); + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("SC_ANALYTICS_GLOBAL_COOKIE=", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task SitecoreLayoutServer_ProxyCookiesFromRequest() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithVisitorIdentificationLayoutPlaceholder)), + }); + + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/AttributeBased", UriKind.Relative)); + request.Headers.Add("Cookie", ["ASP.NET_SessionId=rku2oxmotbrkwkfxe0cpfrvn; path=/; HttpOnly; SameSite=Lax", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly"]); + + // Act + await client.SendAsync(request); + + HttpRequestMessage lsRequest = _mockClientHandler.Requests.First(); + + lsRequest.Headers.GetValues("Cookie").Should().HaveCount(2); + lsRequest.Headers.GetValues("Cookie").Should().Contain(i => i.Contains("ASP.NET_SessionId=", StringComparison.OrdinalIgnoreCase)); + lsRequest.Headers.GetValues("Cookie").Should().Contain(i => i.Contains("SC_ANALYTICS_GLOBAL_COOKIE=", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task SitecoreRequests_ToLayouts_MustIncludeVisitorIdentificationJs() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithVisitorIdentificationLayoutPlaceholder)), + }); + + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/AttributeBased", UriKind.Relative)); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Asserts + string content = await response.Content.ReadAsStringAsync(); + + content.Should().Contain(" + { + builder.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor; + }); + + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + + builder.AddSitecoreRenderingEngine(options => + { + options + .AddDefaultComponentRenderer(); + }) + .WithTracking(); + + builder.AddSitecoreVisitorIdentification(o => o.SitecoreInstanceUri = _cmInstanceUri); + }) + .Configure(app => + { + app.UseForwardedHeaders(); + app.UseSitecoreVisitorIdentification(); + app.UseSitecoreRenderingEngine(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task SitecoreResponse_WithRobotDetection_MustIncludeVisitorIdentificationJs() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithVisitorIdentificationLayoutPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/", UriKind.Relative)); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Asserts + string content = await response.Content.ReadAsStringAsync(); + + content.Should().Contain(""); + } + + [Fact] + public async Task SitecoreRenderingHostResponseMetadata_ProxyCookiesWithLSResponse() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)), + Headers = + { + { "Set-Cookie", AspSessionId }, + { "Set-Cookie", AnalyticsCookie } + } + }); + + HttpClient client = _server.CreateClient(); + + // Act + HttpRequestMessage browserRequest = new(HttpMethod.Get, new Uri("/", UriKind.Relative)); + browserRequest.Headers.Add("cookie", ["ASP.NET_SessionId=rku2oxmotbrkwkfxe0cpfrvn; path=/; HttpOnly; SameSite=Lax", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly"]); + browserRequest.Headers.Add("user-agent", "testUserAgentValue"); + browserRequest.Headers.Add("referer", "testRefererValue"); + browserRequest.Headers.Add("x-forwarded-proto", "https"); + browserRequest.Headers.Add("x-original-proto", "https"); + HttpResponseMessage response = await client.SendAsync(browserRequest); + + // Assert + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("ASP.NET_SessionId=", StringComparison.OrdinalIgnoreCase)); + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("SC_ANALYTICS_GLOBAL_COOKIE=", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task SitecoreLayoutServer_ProxyCookiesFromRequest() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + HttpRequestMessage browserRequest = new(HttpMethod.Get, new Uri("/", UriKind.Relative)); + browserRequest.Headers.Add("Cookie", ["ASP.NET_SessionId=rku2oxmotbrkwkfxe0cpfrvn; path=/; HttpOnly; SameSite=Lax", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly"]); + + // Act + await client.SendAsync(browserRequest); + + HttpRequestMessage lsRequest = _mockClientHandler.Requests.First(); + + // Assert + lsRequest.Headers.GetValues("Cookie").Should().HaveCount(2); + lsRequest.Headers.GetValues("Cookie").Should().Contain(i => i.Contains("ASP.NET_SessionId=", StringComparison.OrdinalIgnoreCase)); + lsRequest.Headers.GetValues("Cookie").Should().Contain(i => i.Contains("SC_ANALYTICS_GLOBAL_COOKIE=", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task SitecoreLayoutServiceRequest_IPAddress_MustBeResolvedCorrectly() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)) + }); + + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/", UriKind.Relative)); + request.Headers.Add("X-Forwarded-For", "192.168.1.0, 172.217.16.14"); + + // Act + await client.SendAsync(request); + HttpRequestMessage lsRequest = _mockClientHandler.Requests.First(); + + lsRequest.Headers.GetValues("X-Forwarded-For").Should().ContainSingle(); + lsRequest.Headers.Contains("X-Forwarded-For").Should().BeTrue(); + lsRequest.Headers.GetValues("X-Forwarded-For").Should().BeEquivalentTo("172.217.16.14"); + } + + [Fact] + public async Task SitecoreLayoutServiceResponseMetadata_ProxyCookies() + { + // Arrange + _mockClientHandler.Responses.Push(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(Serializer.Serialize(CannedResponses.WithNestedPlaceholder)), + Headers = + { + { "Set-Cookie", AspSessionId }, + { "Set-Cookie", AnalyticsCookie } + } + }); + + HttpClient client = _server.CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(new Uri("/", UriKind.Relative)); + + // Assert + response.Headers.GetValues("Set-Cookie").Should().HaveCount(2); + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("ASP.NET_SessionId=", StringComparison.OrdinalIgnoreCase)); + response.Headers.GetValues("Set-Cookie").Should().Contain(i => i.StartsWith("SC_ANALYTICS_GLOBAL_COOKIE=", StringComparison.OrdinalIgnoreCase)); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/TrackingProxyFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/TrackingProxyFixture.cs new file mode 100644 index 0000000..74bee0a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Tracking/TrackingProxyFixture.cs @@ -0,0 +1,97 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Mocks; +using Sitecore.AspNetCore.SDK.Tracking; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Tracking; + +public class TrackingProxyFixture : IDisposable +{ + private readonly TestServer _server; + private readonly HttpLayoutClientMessageHandler _mockClientHandler = new(); + private readonly Uri _layoutServiceUri = new("http://layout.service"); + private readonly Uri _cmInstanceUri = new("http://layout.service"); + + public TrackingProxyFixture() + { + TestServerBuilder testHostBuilder = new(); + _mockClientHandler.Responses.Push(new HttpResponseMessage(HttpStatusCode.OK)); + + _ = testHostBuilder + .ConfigureServices(builder => + { + builder.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + + builder + .AddSitecoreLayoutService() + .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) + .AsDefaultHandler(); + + builder.AddSitecoreRenderingEngine(options => + { + options.AddDefaultComponentRenderer(); + }) + .WithTracking(); + + builder.AddSitecoreVisitorIdentification(options => + { + options.SitecoreInstanceUri = _cmInstanceUri; + }); + + builder.AddSingleton(_ => + { + return new CustomHttpClientFactory( + () => + new HttpClient(_mockClientHandler)); + }); + }) + .Configure(app => + { + app.UseForwardedHeaders(); + app.UseSitecoreVisitorIdentification(); + app.UseSitecoreRenderingEngine(); + }); + + _server = testHostBuilder.BuildServer(new Uri("http://localhost")); + } + + [Fact] + public async Task SitecoreRequests_ToLayouts_MustBeProxied() + { + // Arrange + HttpClient client = _server.CreateClient(); + HttpRequestMessage request = new(HttpMethod.Get, new Uri("/layouts/System/VisitorIdentification.js", UriKind.Relative)); + request.Headers.Add("Cookie", ["ASP.NET_SessionId=rku2oxmotbrkwkfxe0cpfrvn; path=/; HttpOnly; SameSite=Lax", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly"]); + request.Headers.Add("X-Forwarded-For", "172.217.16.14"); + + // Act + await client.SendAsync(request); + + // Asserts + _mockClientHandler.Requests.Should().ContainSingle("A call to rendering middleware is not expected."); + _mockClientHandler.Requests[0].RequestUri!.Host.Should().Be(_cmInstanceUri.Host); + _mockClientHandler.Requests[0].RequestUri!.Scheme.Should().Be(_cmInstanceUri.Scheme); + _mockClientHandler.Requests[0].RequestUri!.PathAndQuery.Should().Be("/layouts/System/VisitorIdentification.js"); + _mockClientHandler.Requests[0].Headers.Should().Contain(h => h.Key.Equals("Cookie")); + _mockClientHandler.Requests[0].Headers.GetValues("x-forwarded-for").First().ToUpperInvariant().Should().Be("172.217.16.14"); + _mockClientHandler.Requests[0].Headers.GetValues("x-forwarded-host").First().ToUpperInvariant().Should().Be("LOCALHOST"); + _mockClientHandler.Requests[0].Headers.GetValues("x-forwarded-proto").First().ToUpperInvariant().Should().Be("HTTP"); + } + + public void Dispose() + { + _server.Dispose(); + _mockClientHandler.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/HttpLayoutClientMessageHandler.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/HttpLayoutClientMessageHandler.cs new file mode 100644 index 0000000..7a6b72a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/HttpLayoutClientMessageHandler.cs @@ -0,0 +1,18 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests; + +public class HttpLayoutClientMessageHandler : HttpMessageHandler +{ + public Stack Responses { get; } = new(); + + public List Requests { get; } = []; + + public bool WasInvoked { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Requests.Add(request); + WasInvoked = true; + HttpResponseMessage response = Responses.Pop(); + return Task.FromResult(response); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLog.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLog.cs new file mode 100644 index 0000000..bd67974 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLog.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Logging; + +public static class InMemoryLog +{ + public static readonly List Log = []; +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLogger.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLogger.cs new file mode 100644 index 0000000..e0ae14e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/InMemoryLogger.cs @@ -0,0 +1,32 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Logging; + +/// +/// An in memory logger for use with Integration Tests. +/// +public class InMemoryLogger : ILogger, IDisposable +{ + private readonly Disposable _disposable = new(); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + InMemoryLog.Log.Add(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) + where TState : notnull => _disposable; + + public void Dispose() + { + _disposable.Dispose(); + GC.SuppressFinalize(this); + } + + private class Disposable : IDisposable + { + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/IntegrationTestLoggerProvider.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/IntegrationTestLoggerProvider.cs new file mode 100644 index 0000000..5774017 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Logging/IntegrationTestLoggerProvider.cs @@ -0,0 +1,11 @@ +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Logging; + +public class IntegrationTestLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) => new InMemoryLogger(); + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Properties/launchSettings.json b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Properties/launchSettings.json new file mode 100644 index 0000000..44204d4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59371;http://localhost:59372" + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj new file mode 100644 index 0000000..6b19193 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs new file mode 100644 index 0000000..55c091e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.TestHost; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests; + +public class TestServerBuilder +{ + private readonly IWebHostBuilder _webHostBuilder = PrepareDefault(); + + private readonly List> _appDelegates = []; + + public TestServer BuildServer(Uri uri) + { + _webHostBuilder.Configure(app => + { + app.UseRouting(); + _appDelegates.ForEach(configure => configure(app)); + app.UseEndpoints(configure => + { + configure.MapDefaultControllerRoute(); + }); + }); + + return new TestServer(_webHostBuilder) + { + BaseAddress = uri + }; + } + + public IWebHost Build() => _webHostBuilder.Build(); + + public TestServerBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _webHostBuilder.ConfigureAppConfiguration(configureDelegate); + return this; + } + + public TestServerBuilder ConfigureServices(Action configureServices) + { + _webHostBuilder.ConfigureServices(configureServices); + return this; + } + + public TestServerBuilder ConfigureServices(Action configureServices) + { + _webHostBuilder.ConfigureServices(configureServices); + return this; + } + + public string? GetSetting(string key) => _webHostBuilder.GetSetting(key); + + public TestServerBuilder UseSetting(string key, string value) + { + _webHostBuilder.UseSetting(key, value); + return this; + } + + public TestServerBuilder Configure(Action configureApp) + { + _appDelegates.Add(configureApp); + return this; + } + + private static IWebHostBuilder PrepareDefault() + { + return WebHost.CreateDefaultBuilder() + .ConfigureServices(services => + { + services + .AddRouting() + .AddSitecoreLayoutService(); + + services.AddSitecoreRenderingEngine(); + }); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewComponents/Component1ViewComponent.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewComponents/Component1ViewComponent.cs new file mode 100644 index 0000000..8760173 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewComponents/Component1ViewComponent.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.ViewComponents; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ViewComponents; + +public class Component1ViewComponent(IViewModelBinder binder) + : BindingViewComponent(binder) +{ + public Task InvokeAsync() + { + return Task.FromResult(View()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreContextViewModel.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreContextViewModel.cs new file mode 100644 index 0000000..310e72c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreContextViewModel.cs @@ -0,0 +1,12 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ViewModels; + +public class WithBoundSitecoreContextViewModel +{ + public Context? SitecoreContext { get; set; } + + public string? Language { get; set; } + + public bool IsPageEditing { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreRouteViewModel.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreRouteViewModel.cs new file mode 100644 index 0000000..185d62f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/ViewModels/WithBoundSitecoreRouteViewModel.cs @@ -0,0 +1,13 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Route = Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Route; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ViewModels; + +public class WithBoundSitecoreRouteViewModel +{ + public Route? SitecoreRoute { get; set; } + + public string? DatabaseName { get; set; } + + public TextField? PageTitle { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreContext.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreContext.cshtml new file mode 100644 index 0000000..91fa9b5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreContext.cshtml @@ -0,0 +1,5 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ViewModels.WithBoundSitecoreContextViewModel + +

Page state: @Model.SitecoreContext?.PageState

+

Language: @Model.Language

+

The page is in edit mode: @Model.IsPageEditing

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreResponse.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreResponse.cshtml new file mode 100644 index 0000000..b4ddcf1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreResponse.cshtml @@ -0,0 +1,3 @@ +@model Sitecore.AspNetCore.SDK.LayoutService.Client.Response.SitecoreLayoutResponse + +

@Model.Content?.Sitecore?.Route?.DatabaseName

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreRoute.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreRoute.cshtml new file mode 100644 index 0000000..3edc7f4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/GlobalMiddleware/WithBoundSitecoreRoute.cshtml @@ -0,0 +1,5 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ViewModels.WithBoundSitecoreRouteViewModel + +

Page title: @Model.PageTitle?.Value

+

Database name: @Model.DatabaseName

+

Item ID: @Model.SitecoreRoute?.ItemId

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/Component1/Default.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/Component1/Default.cshtml new file mode 100644 index 0000000..8f833b4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/Component1/Default.cshtml @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComplexComponent.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComplexComponent.cshtml new file mode 100644 index 0000000..e596f26 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComplexComponent.cshtml @@ -0,0 +1,16 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComplexComponent + +
+

+

+

+

+

+
+ + + + + +

@Model.ParamName

+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component2.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component2.cshtml new file mode 100644 index 0000000..263e199 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component2.cshtml @@ -0,0 +1,8 @@ +@using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels +@{ + Component2? component = this.SitecoreComponent()?.ReadFields(); +} +
+ + +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component3.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component3.cshtml new file mode 100644 index 0000000..df18548 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component3.cshtml @@ -0,0 +1,9 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.Component3 + +
+

+
+

+ + +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.cshtml new file mode 100644 index 0000000..642a6e2 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.cshtml @@ -0,0 +1,11 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.Component4 + +
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.da.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.da.cshtml new file mode 100644 index 0000000..9f2276a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component4.da.cshtml @@ -0,0 +1,11 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.Component4 + +
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component5.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component5.cshtml new file mode 100644 index 0000000..bfff857 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component5.cshtml @@ -0,0 +1,9 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.Component5 + +
+

+
+

+ + +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component6.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component6.cshtml new file mode 100644 index 0000000..fc91589 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/Component6.cshtml @@ -0,0 +1,7 @@ +@using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels +@{ + Component6? component = this.SitecoreComponent()?.ReadFields(); +} +
+ +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithAllFieldTypes.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithAllFieldTypes.cshtml new file mode 100644 index 0000000..9155975 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithAllFieldTypes.cshtml @@ -0,0 +1,13 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithAllFieldTypes + +
+

+
+
+
+
+
+
+
Text
+
+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithDates.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithDates.cshtml new file mode 100644 index 0000000..1376f9c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithDates.cshtml @@ -0,0 +1,9 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithDates + +
+

+

+ + +

+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithFIles.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithFIles.cshtml new file mode 100644 index 0000000..15c60d1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithFIles.cshtml @@ -0,0 +1,23 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithFiles + +
+ + + + + + + + + + + Custom Download Link + Custom Download Link + Custom Download Link + Custom Download Link> + +

Inner html

+

Inner html

+

Inner html

+

Inner html

+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithImages.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithImages.cshtml new file mode 100644 index 0000000..c6d2dbc --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithImages.cshtml @@ -0,0 +1,7 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithImages + +
+ +
+ +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithLinks.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithLinks.cshtml new file mode 100644 index 0000000..2b0993a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithLinks.cshtml @@ -0,0 +1,11 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithLinks + + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithMissingData.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithMissingData.cshtml new file mode 100644 index 0000000..96345fb --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithMissingData.cshtml @@ -0,0 +1,10 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithMissingData + +
+

+

+

+

+ + +

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithNumber.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithNumber.cshtml new file mode 100644 index 0000000..94b490e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithNumber.cshtml @@ -0,0 +1,11 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithNumber + +
+
    +
  • Number Default Culture With Formatting: +
  • Number Specific Culture With Formatting: +
  • Number Without Formatting: +
  • Span:
  • +
+ +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithoutId.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithoutId.cshtml new file mode 100644 index 0000000..ea8fefe --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/ComponentWithoutId.cshtml @@ -0,0 +1,6 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.ComponentWithoutId +
+
+

+

+

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomModelContextComponent.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomModelContextComponent.cshtml new file mode 100644 index 0000000..29d37cf --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Components/SitecoreComponent/CustomModelContextComponent.cshtml @@ -0,0 +1,15 @@ +@model Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels.CustomModelContextComponent + +
+

@Model.CustomContext?.SingleProperty

+

@Model.CustomContext?.TestClass1?.TestString

+

@Model.CustomContext?.TestClass1?.TestInt

+

@Model.CustomContext?.TestClass1?.TestTime

+

@Model.CustomContext?.TestClass2?.TestString

+

@Model.CustomContext?.TestClass2?.TestInt

+

@Model.TestClass1?.TestInt

+

@Model.TestClass1?.TestString

+

@Model.CustomContextIndividual?.TestClass1?.TestInt

+

@Model.CustomContextIndividual?.TestClass1?.TestString

+

@Model.SingleProperty

+
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Index.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Index.cshtml new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/Index.cshtml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/NestedPlaceholderPageLayout.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/NestedPlaceholderPageLayout.cshtml new file mode 100644 index 0000000..8256f29 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/NestedPlaceholderPageLayout.cshtml @@ -0,0 +1,2 @@ +

Nested Placeholder Page Layout

+ \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/RelativeUrls.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/RelativeUrls.cshtml new file mode 100644 index 0000000..6811b75 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/RelativeUrls.cshtml @@ -0,0 +1,2 @@ +@using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewForHandlingError.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewForHandlingError.cshtml new file mode 100644 index 0000000..325509b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewForHandlingError.cshtml @@ -0,0 +1 @@ +

@ViewData["ErrorMessage"]

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewWithMissingComponent.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewWithMissingComponent.cshtml new file mode 100644 index 0000000..14ee8d9 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/ViewWithMissingComponent.cshtml @@ -0,0 +1,2 @@ +

MissingComponent Page Layout

+ \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/VisitorIdentificationLayout.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/VisitorIdentificationLayout.cshtml new file mode 100644 index 0000000..87a3e8d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/VisitorIdentificationLayout.cshtml @@ -0,0 +1,3 @@ + +

Nested Placeholder Page Layout

+ \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_ComponentNotFound.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_ComponentNotFound.cshtml new file mode 100644 index 0000000..8053735 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_ComponentNotFound.cshtml @@ -0,0 +1 @@ +

ComponentIsMissing

\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_PartialView.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_PartialView.cshtml new file mode 100644 index 0000000..fc91589 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/Shared/_PartialView.cshtml @@ -0,0 +1,7 @@ +@using Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.ComponentModels +@{ + Component6? component = this.SitecoreComponent()?.ReadFields(); +} +
+ +
\ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/_ViewImports.cshtml b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/_ViewImports.cshtml new file mode 100644 index 0000000..3b951ae --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Sitecore.AspNetCore.SDK.RenderingEngine +@addTagHelper *, Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification +@using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.Development.json b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.json b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Attributes/UseSitecoreRenderingAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Attributes/UseSitecoreRenderingAttributeFixture.cs new file mode 100644 index 0000000..009da93 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Attributes/UseSitecoreRenderingAttributeFixture.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Attributes; + +public class UseSitecoreRenderingAttributeFixture +{ + [Fact] + public void Ctor_NullType_ThrowsException() + { + // Arrange + Action action = () => _ = new UseSitecoreRenderingAttribute(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("configurationType"); + } + + [Fact] + public void Ctor_WithType_SetsType() + { + // Arrange / Act + UseSitecoreRenderingAttribute sut = new(typeof(Controller)); + + // Assert + sut.ConfigurationType.Should().Be(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldAttributeFixture.cs new file mode 100644 index 0000000..a6fe8f7 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreComponentFieldAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreComponentFieldAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreComponentFieldAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldsAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldsAttributeFixture.cs new file mode 100644 index 0000000..50b49c5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentFieldsAttributeFixture.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreComponentFieldsAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreComponentFieldsAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentParameterAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentParameterAttributeFixture.cs new file mode 100644 index 0000000..d0a24f9 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentParameterAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreComponentParameterAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreComponentParameterAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreComponentParameterAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentPropertyAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentPropertyAttributeFixture.cs new file mode 100644 index 0000000..b2e5e72 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreComponentPropertyAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreComponentPropertyAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreComponentPropertyAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreComponentPropertyAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextAttributeFixture.cs new file mode 100644 index 0000000..7caf4d4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextAttributeFixture.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreContextAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreContextAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextPropertyAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextPropertyAttributeFixture.cs new file mode 100644 index 0000000..c50a321 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreContextPropertyAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreContextPropertyAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreContextPropertyAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreContextPropertyAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreLayoutResponseAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreLayoutResponseAttributeFixture.cs new file mode 100644 index 0000000..31a05a5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreLayoutResponseAttributeFixture.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreLayoutResponseAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreLayoutResponseAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldAttributeFixture.cs new file mode 100644 index 0000000..14b9916 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreRouteFieldAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreRouteFieldAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreRouteFieldAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldsAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldsAttributeFixture.cs new file mode 100644 index 0000000..07a4a4f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRouteFieldsAttributeFixture.cs @@ -0,0 +1,17 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreRouteFieldsAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreRouteFieldsAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRoutePropertyAttributeFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRoutePropertyAttributeFixture.cs new file mode 100644 index 0000000..a27ffb6 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Attributes/SitecoreRoutePropertyAttributeFixture.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Attributes; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Attributes; + +public class SitecoreRoutePropertyAttributeFixture +{ + [Theory] + [AutoNSubstituteData] + public void Attribute_Name_ShouldNotBeNull(SitecoreRoutePropertyAttribute sut) + { + // Assert + sut.Name.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Attribute_BindingSource_ShouldNotBeNull(SitecoreRoutePropertyAttribute sut) + { + // Assert + sut.BindingSource.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/BindingViewComponentFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/BindingViewComponentFixture.cs new file mode 100644 index 0000000..a434445 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/BindingViewComponentFixture.cs @@ -0,0 +1,116 @@ +using System.Text; +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Rendering; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding; + +public class BindingViewComponentFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup() + { + return f => + { + IViewModelBinder? binder = Substitute.For(); + + f.Inject(binder); + }; + } + + [Fact] + public void BindingViewComponent_Guarded() + { + // Arrange + Func act = + () => new BindingViewComponentMock(null!); + + // Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void BindingViewComponent_BinderPropertySet( + IViewModelBinder viewModelBinder) + { + // Act + BindingViewComponentMock sut = new(viewModelBinder); + + // Assert + sut.BinderAccessor.Should().Be(viewModelBinder); + } + + [Theory] + [AutoNSubstituteData] + public async Task BindView__WithT_BinderCalled( + IViewModelBinder viewModelBinder, + IViewModelBinder binder) + { + // Arrange + BindingViewComponentMock sut = new(viewModelBinder); + + // Act + await sut.BindView(); + + // Assert + await binder.Received(1).Bind(Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task BindView_WithModel_BinderCalled( + IViewModelBinder viewModelBinder, + IViewModelBinder binder, + StringBuilder model) + { + // Arrange + BindingViewComponentMock sut = new(viewModelBinder); + + // Act + await sut.BindView(model); + + // Assert + await binder.Received(1).Bind(model, Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task BindView_WithViewName_BinderCalled( + IViewModelBinder viewModelBinder, + IViewModelBinder binder, + string viewName) + { + // Arrange + BindingViewComponentMock sut = new(viewModelBinder); + + // Act + await sut.BindView(viewName); + + // Assert + await binder.Received(1).Bind(Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task BindView_WithModelAndViewName_BinderCalled( + IViewModelBinder viewModelBinder, + IViewModelBinder binder, + string viewName, + StringBuilder model) + { + // Arrange + BindingViewComponentMock sut = new(viewModelBinder); + + // Act + await sut.BindView(viewName, model); + + // Assert + await binder.Received(1).Bind(model, Arg.Any()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Extensions/BindingExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Extensions/BindingExtensionsFixture.cs new file mode 100644 index 0000000..f565e36 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Extensions/BindingExtensionsFixture.cs @@ -0,0 +1,135 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Extensions; + +public class BindingExtensionsFixture +{ + [Fact] + public void GetModelBinder_WithTSourceAndTType_IsGuarded() + { + // Arrange + Func act = + () => BindingExtensions.GetModelBinder(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void GetModelBinder_WithTSource_IsGuarded() + { + // Arrange + Func act = + () => BindingExtensions.GetModelBinder(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void AddSitecoreModelBinderProviders_IsGuarded() + { + // Arrange + Action act = + () => BindingExtensions.AddSitecoreModelBinderProviders(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Fact] + public void GetModelBinder_WithValidBindingSource_ReturnsCorrectModelBinder() + { + // Arrange + TestModelMetadata testMetadata = new(typeof(object)); + BindingInfo bindingInfo = new() + { + BindingSource = new TestSitecoreLayoutBindingSource() + }; + TestModelBinderProviderContext context = new(testMetadata, bindingInfo); + + // Act + BinderTypeModelBinder? result = context.GetModelBinder(); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void GetModelBinder_WithInvalidBindingSource_ReturnsNull() + { + // Arrange + TestModelMetadata testMetadata = new(typeof(object)); + BindingInfo bindingInfo = new() + { + BindingSource = new TestBindingSource() + }; + TestModelBinderProviderContext context = new(testMetadata, bindingInfo); + + // Act + BinderTypeModelBinder? result = context.GetModelBinder(); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void GetModelBinder_WithValidModelTypeAndBindingSource_ReturnsCorrectModelBinder() + { + // Arrange + TestModelMetadata testMetadata = new(typeof(string)); + BindingInfo bindingInfo = new() + { + BindingSource = new TestSitecoreLayoutBindingSource() + }; + TestModelBinderProviderContext context = new(testMetadata, bindingInfo); + + // Act + BinderTypeModelBinder? result = context.GetModelBinder(); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void GetModelBinder_WithInvalidModelTypeAndBindingSource_ReturnsNull() + { + // Arrange + TestModelMetadata testMetadata = new(typeof(object)); + BindingInfo bindingInfo = new() + { + BindingSource = new TestBindingSource() + }; + TestModelBinderProviderContext context = new(testMetadata, bindingInfo); + + // Act + BinderTypeModelBinder? result = context.GetModelBinder(); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void AddSitecoreModelBinderProviders_MvcOptions_Contain_ExpectedProviders() + { + // Arrange + MvcOptions options = new(); + + // Act + options.AddSitecoreModelBinderProviders(); + + // Assert + options.ModelBinderProviders.Should().HaveCount(4); + options.ModelBinderProviders[0].Should().BeOfType(typeof(SitecoreLayoutRouteModelBinderProvider)); + options.ModelBinderProviders[1].Should().BeOfType(typeof(SitecoreLayoutContextModelBinderProvider)); + options.ModelBinderProviders[2].Should().BeOfType(typeof(SitecoreLayoutComponentModelBinderProvider)); + options.ModelBinderProviders[3].Should().BeOfType(typeof(SitecoreLayoutResponseModelBinderProvider)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutComponentModelBinderProviderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutComponentModelBinderProviderFixture.cs new file mode 100644 index 0000000..1d38c99 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutComponentModelBinderProviderFixture.cs @@ -0,0 +1,110 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Providers; + +public class SitecoreLayoutComponentModelBinderProviderFixture +{ + public static Action ExpectedBindingSource => f => + { + SitecoreLayoutComponentFieldBindingSource? bindingSource = Substitute.For("test", "test", false, false); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action UnknownBindingSource => f => + { + BindingSource? bindingSource = Substitute.For("test", "test", true, true); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + [Theory] + [AutoNSubstituteData] + public void GetBinder_NullContext_Throws(SitecoreLayoutComponentModelBinderProvider sut) + { + // Arrange + Func act = + () => sut.GetBinder(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData(nameof(ExpectedBindingSource))] + public void GetBinder_WithSitecoreLayoutComponentFieldBindingSource_ReturnsBinder( + SitecoreLayoutComponentModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(UnknownBindingSource))] + public void GetBinder_WithUnknownBindingSource_ReturnsNull( + SitecoreLayoutComponentModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().BeNull(); + } + + [Theory] + [InlineAutoNSubstituteData(typeof(Field))] + [InlineAutoNSubstituteData(typeof(Field))] + [InlineAutoNSubstituteData(typeof(CheckboxField))] + [InlineAutoNSubstituteData(typeof(ContentListField))] + [InlineAutoNSubstituteData(typeof(DateField))] + [InlineAutoNSubstituteData(typeof(FileField))] + [InlineAutoNSubstituteData(typeof(HyperLinkField))] + [InlineAutoNSubstituteData(typeof(ImageField))] + [InlineAutoNSubstituteData(typeof(ItemLinkField))] + [InlineAutoNSubstituteData(typeof(NumberField))] + [InlineAutoNSubstituteData(typeof(RichTextField))] + [InlineAutoNSubstituteData(typeof(TextField))] + public void GetBinder_WithFieldModelType_ReturnsBinder( + Type fieldType, + SitecoreLayoutComponentModelBinderProvider sut) + { + // Arrange + TestModelMetadata modelMetadata = new(fieldType); + TestModelBinderProviderContext modelBinderProviderContext = new(modelMetadata); + + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutContextModelBinderProviderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutContextModelBinderProviderFixture.cs new file mode 100644 index 0000000..e4e2728 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutContextModelBinderProviderFixture.cs @@ -0,0 +1,122 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Providers; + +public class SitecoreLayoutContextModelBinderProviderFixture +{ + public static Action SitecoreLayoutContextBindingSource => f => + { + SitecoreLayoutContextBindingSource? bindingSource = Substitute.For(); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action SitecoreLayoutContextPropertyBindingSource => f => + { + SitecoreLayoutContextPropertyBindingSource? bindingSource = Substitute.For("test"); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action UnknownBindingSource => f => + { + BindingSource? bindingSource = Substitute.For("test", "test", true, true); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + [Theory] + [AutoNSubstituteData] + public void GetBinder_NullContext_Throws(SitecoreLayoutContextModelBinderProvider sut) + { + Func act = + () => sut.GetBinder(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData(nameof(SitecoreLayoutContextBindingSource))] + public void GetBinder_WithSitecoreLayoutContextBindingSource_ReturnsBinder( + SitecoreLayoutContextModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(SitecoreLayoutContextPropertyBindingSource))] + public void GetBinder_WithSitecoreLayoutContextPropertyBindingSource_ReturnsBinder( + SitecoreLayoutContextModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(UnknownBindingSource))] + public void GetBinder_WithUnknownBindingSource_ReturnsNull( + SitecoreLayoutContextModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetBinder_WithContextModelType_ReturnsBinder( + SitecoreLayoutContextModelBinderProvider sut) + { + // Arrange + TestModelMetadata modelMetadata = new(typeof(Context)); + TestModelBinderProviderContext modelBinderProviderContext = new(modelMetadata); + + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutResponseModelBinderProviderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutResponseModelBinderProviderFixture.cs new file mode 100644 index 0000000..aa7bc40 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutResponseModelBinderProviderFixture.cs @@ -0,0 +1,76 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Providers; + +public class SitecoreLayoutResponseModelBinderProviderFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseBindingSource? bindingSource = Substitute.For(); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action UnknownBindingSource => f => + { + BindingSource? bindingSource = Substitute.For("test", "test", true, true); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + [Theory] + [AutoNSubstituteData] + public void GetBinder_NullContext_Throws(SitecoreLayoutResponseModelBinderProvider sut) + { + // Arrange + Func act = + () => sut.GetBinder(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetBinder_WithSitecoreLayoutBindingSource_ReturnsBinder( + SitecoreLayoutResponseModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(UnknownBindingSource))] + public void GetBinder_WithUnknownBindingSource_ReturnsNull( + SitecoreLayoutResponseModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + binder.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutRouteModelBinderProviderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutRouteModelBinderProviderFixture.cs new file mode 100644 index 0000000..6909e89 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Providers/SitecoreLayoutRouteModelBinderProviderFixture.cs @@ -0,0 +1,167 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Providers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Providers; + +public class SitecoreLayoutRouteModelBinderProviderFixture +{ + public static Action SitecoreLayoutRouteBindingSource => f => + { + SitecoreLayoutRouteBindingSource? bindingSource = Substitute.For(); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action SitecoreLayoutRouteFieldBindingSource => f => + { + SitecoreLayoutRouteFieldBindingSource? bindingSource = Substitute.For("test"); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action SitecoreLayoutRoutePropertyBindingSource => f => + { + SitecoreLayoutRoutePropertyBindingSource? bindingSource = Substitute.For("test"); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + public static Action UnknownBindingSource => f => + { + BindingSource? bindingSource = Substitute.For("test", "test", true, true); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + }; + + [Theory] + [AutoNSubstituteData] + public void GetBinder_NullContext_Throws(SitecoreLayoutRouteModelBinderProvider sut) + { + // Arrange + Func act = + () => sut.GetBinder(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData(nameof(SitecoreLayoutRouteBindingSource))] + public void GetBinder_WithSitecoreLayoutRouteBindingSource_ReturnsBinder( + SitecoreLayoutRouteModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(SitecoreLayoutRouteFieldBindingSource))] + public void GetBinder_WithSitecoreLayoutRouteFieldBindingSource_ReturnsBinder( + SitecoreLayoutRouteModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(SitecoreLayoutRoutePropertyBindingSource))] + public void GetBinder_WithSitecoreLayoutRoutePropertyBindingSource_ReturnsBinder( + SitecoreLayoutRouteModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData(nameof(UnknownBindingSource))] + public void GetBinder_WithUnknownBindingSource_ReturnsNull( + SitecoreLayoutRouteModelBinderProvider sut, + ModelBinderProviderContext modelBinderProviderContext) + { + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetBinder_WithRouteModelType_ReturnsBinder( + SitecoreLayoutRouteModelBinderProvider sut) + { + // Arrange + TestModelMetadata modelMetadata = new(typeof(Route)); + TestModelBinderProviderContext modelBinderProviderContext = new(modelMetadata); + + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetBinder_WithRouteOfTModelType_ReturnsBinder( + SitecoreLayoutRouteModelBinderProvider sut) + { + // Arrange + TestModelMetadata modelMetadata = new(typeof(Route)); + TestModelBinderProviderContext modelBinderProviderContext = new(modelMetadata); + + // Act + IModelBinder? binder = sut.GetBinder(modelBinderProviderContext); + + // Assert + binder.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreLayoutModelBinderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreLayoutModelBinderFixture.cs new file mode 100644 index 0000000..70113ad --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreLayoutModelBinderFixture.cs @@ -0,0 +1,72 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding; + +public class SitecoreLayoutModelBinderFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ActionContext actionContext = new( + new DefaultHttpContext(), + new Microsoft.AspNetCore.Routing.RouteData(), + new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()); + + f.Inject(actionContext); + IServiceProvider? sp = f.Freeze(); + IServiceScopeFactory? scopeFactory = f.Freeze(); + sp.GetService(typeof(IServiceScopeFactory)).Returns(scopeFactory); + }; + + [Theory] + [AutoNSubstituteData] + public async Task BindModelAsync_WithNullModelBindingContext_Throws(SitecoreLayoutModelBinder sut) + { + Func act = + () => sut.BindModelAsync(null!); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public void BindModelAsync_WithNullModelReturnedByBindingSource_ReturnsNoModel( + SitecoreLayoutModelBinder sut, + DefaultModelBindingContext defaultModelBindingContext) + { + // Act + sut.BindModelAsync(defaultModelBindingContext); + + // Assert + defaultModelBindingContext.Result.IsModelSet.Should().BeFalse(); + } + + [Theory] + [AutoNSubstituteData] + public void BindModelAsync_WithNullModelReturnedByBindingSource_WritesWarningLogs( + DefaultModelBindingContext defaultModelBindingContext, + IServiceProvider sp) + { + // Arrange + ILogger>? substituteLogger = Substitute.For>>(); + SitecoreLayoutModelBinder modelBinder = new(sp, substituteLogger); + + // Act + modelBinder.BindModelAsync(defaultModelBindingContext); + + // Assert + substituteLogger.Received(1).Log>>(LogLevel.Warning, 0, Arg.Any>>(), null, Arg.Any>()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreViewModelBinderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreViewModelBinderFixture.cs new file mode 100644 index 0000000..1272f5a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/SitecoreViewModelBinderFixture.cs @@ -0,0 +1,150 @@ +using System.Text; +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding; + +public class SitecoreViewModelBinderFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ControllerBase? controller = Substitute.For(); + SitecoreRenderingContext renderingContext = new() + { + Response = new SitecoreLayoutResponse([]) { Content = CannedResponses.StyleGuide1 }, + Component = new Component(), + Controller = controller + }; + + FeatureCollection featureCollection = new(); + featureCollection.Set(renderingContext); + + ActionContext actionContext = new( + new DefaultHttpContext(featureCollection), + new Microsoft.AspNetCore.Routing.RouteData(), + new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()); + + BindingSource? bindingSource = Substitute.For("dds", "sds", true, true); + + bindingSource.CanAcceptDataFrom(Arg.Any()).Returns(true); + + BindingInfo bindingInfo = new() + { + BindingSource = bindingSource + }; + + f.Inject(bindingInfo); + f.Inject(actionContext); + f.Inject(renderingContext); + f.Inject(controller); + }; + + [Theory] + [AutoNSubstituteData] + internal async Task Bind_NullContext_Throws(SitecoreViewModelBinder sut) + { + Func> act = + () => sut.Bind(null!); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + internal void Bind_WithViewContext_ReturnsModel( + SitecoreViewModelBinder sut, + ViewContext viewContext) + { + Task model = sut.Bind(viewContext); + + model.Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Bind_NullModel_Throws(SitecoreViewModelBinder sut) + { + Func act = + () => sut.Bind(null!, null!); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Bind_ModelWithNullViewContext_Throws(SitecoreViewModelBinder sut) + { + Func act = + () => sut.Bind(new SitecoreViewModelBinder(), null!); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Bind_WithModelAndViewContext_ReturnsModel( + SitecoreViewModelBinder sut, + ViewContext viewContext, + string model, + ControllerBase controller) + { + await sut.Bind(model, viewContext); + + await controller.Received(1).TryUpdateModelAsync(Arg.Is(model)); + } + + [Theory] + [AutoNSubstituteData] + internal async Task BindUsingType_NullModelType_Throws(SitecoreViewModelBinder sut) + { + // Arrange + Func> act = + () => sut.Bind(null!, null!); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("Value cannot be null. (Parameter 'modelType')"); + } + + [Theory] + [AutoNSubstituteData] + internal async Task BindUsingType_NullViewContext_Throws(SitecoreViewModelBinder sut) + { + // Arrange + Func> act = + () => sut.Bind(typeof(StringBuilder), null!); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("Value cannot be null. (Parameter 'viewContext')"); + } + + [Theory] + [AutoNSubstituteData] + internal async Task BindUsingType_WithModelAndViewContext_ReturnsModel( + SitecoreViewModelBinder sut, + ViewContext viewContext, + ControllerBase controller) + { + await sut.Bind(typeof(ContentBlock), viewContext); + + await controller.Received(1) + .TryUpdateModelAsync( + Arg.Is(o => o.GetType() == typeof(ContentBlock)), + Arg.Is(typeof(ContentBlock)), + Arg.Is(string.Empty)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentBindingSourceFixture.cs new file mode 100644 index 0000000..9659e11 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentBindingSourceFixture.cs @@ -0,0 +1,126 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutComponentBindingSourceFixture +{ + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutComponentBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullComponent_ReturnsNull( + SitecoreLayoutComponentBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = null + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithComponentType_ReturnsModel( + SitecoreLayoutComponentBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(Component))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().Be(renderingContext.Component); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidModel_ReturnsNull( + SitecoreLayoutComponentBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? result = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + result.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldBindingSourceFixture.cs new file mode 100644 index 0000000..41c45ae --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldBindingSourceFixture.cs @@ -0,0 +1,320 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutComponentFieldBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutComponentFieldBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullComponent_ReturnsNull( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = null + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullFieldName_ReturnsNull( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyFieldName_ReturnsNull( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullFieldName_BindingContextFieldNameIsUsedForBinding( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = null; + modelBindingContext.FieldName = "heading1"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("HeaderBlock - This is heading1"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_FieldNameIsSet_PropertyNameIsUsedForBinding( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "heading1"; + modelBindingContext.FieldName = "someParam"; + + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, context); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("HeaderBlock - This is heading1"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownFieldName_ReturnsNull( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidFieldName"; + modelBindingContext.FieldName = "invalidFieldName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidFieldName_ReturnsModel( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "heading1"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("HeaderBlock - This is heading1"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidType_ReturnsNull( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "heading1"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_ComponentWithoutDatasource_ReturnsContextItemFieldValue_IfFieldsMatch_WithContextItem( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Component!.DataSource = null!; + renderingContext.Component.Fields = []; + sut.Name = "pageTitle"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("Styleguide | Sitecore JSS"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_ComponentWithoutDatasource_ReturnsNull_IfFieldsTypesDoNotMatch_WithContextItem( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(NumberField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Component!.DataSource = null!; + renderingContext.Component.Fields = []; + sut.Name = "pageTitle"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_ComponentWithoutDatasource_ReturnsNull_IfFieldsNamesDoNotMatch_WithContextItem( + SitecoreLayoutComponentFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Component!.DataSource = null!; + renderingContext.Component.Fields = []; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldsBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldsBindingSourceFixture.cs new file mode 100644 index 0000000..16f3d5c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentFieldsBindingSourceFixture.cs @@ -0,0 +1,139 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutComponentFieldsBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutComponentFieldsBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullComponent_ReturnsNull( + SitecoreLayoutComponentFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = null + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidFieldsModel_ReturnsPopulatedModel( + SitecoreLayoutComponentFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(HeaderBlockFields))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(HeaderBlockFields)); + HeaderBlockFields? fieldsModel = model as HeaderBlockFields; + fieldsModel!.Heading1!.Value.Should().Be("HeaderBlock - This is heading1"); + fieldsModel.Heading2!.Value.Should().Be("HeaderBlock - This is heading2"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidFieldsModel_ReturnsNull( + SitecoreLayoutComponentFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + private class HeaderBlockFields + { + public TextField? Heading1 { get; set; } + + public TextField? Heading2 { get; set; } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentParameterBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentParameterBindingSourceFixture.cs new file mode 100644 index 0000000..dce5e4f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentParameterBindingSourceFixture.cs @@ -0,0 +1,253 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutComponentParameterBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuideWithComponentParameters; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutComponentParameterBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullComponent_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = null + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullParameters_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = new Component + { + Parameters = null! + } + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyParameters_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = new Component + { + Parameters = [] + } + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyPropertyName_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_FieldNameIsUsedForBinding( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = "CmpParam1"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("Value1"); + } + + [Theory] + [InlineAutoNSubstituteData("CmpParam1")] + [InlineAutoNSubstituteData("cmpParam1")] + [InlineAutoNSubstituteData("cmpparam1")] + [InlineAutoNSubstituteData("CMPPARAM1")] + public void GetModel_PropertyNameIsSet_PropertyNameIsUsedForBinding( + string name, + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = name; + modelBindingContext.FieldName = "componentName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("Value1"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownPropertyName_ReturnsNull( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidName"; + modelBindingContext.FieldName = "componentInvalidName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidPropertyName_ReturnsModel( + SitecoreLayoutComponentParameterBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "CmpParam1"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("Value1"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentPropertyBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentPropertyBindingSourceFixture.cs new file mode 100644 index 0000000..9df41ee --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutComponentPropertyBindingSourceFixture.cs @@ -0,0 +1,247 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutComponentPropertyBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutComponentPropertyBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullComponent_ReturnsNull( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreRenderingContext renderingContext = new() + { + Component = null + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_ReturnsNull( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyPropertyName_ReturnsNull( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_FieldNameIsUsedForBinding( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = "Name"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("HeaderBlock"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_PropertyNameIsSet_PropertyNameIsUsedForBinding( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "Name"; + modelBindingContext.FieldName = "componentName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("HeaderBlock"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownPropertyName_ReturnsNull( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidName"; + modelBindingContext.FieldName = "componentInvalidName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidPropertyName_ReturnsModel( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "Name"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("HeaderBlock"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithWithRightModelFieldType_ReturnsPropertyValue( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + modelBindingContext.ModelType.ReturnsForAnyArgs(typeof(string)); + + // Arrange + sut.Name = "Name"; + modelBindingContext.FieldName = "componentName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("HeaderBlock"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithWithWrongModelFieldType_ReturnsNull( + SitecoreLayoutComponentPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + modelBindingContext.ModelType.ReturnsForAnyArgs(typeof(int)); + + // Arrange + sut.Name = "Name"; + modelBindingContext.FieldName = "componentName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextBindingSourceFixture.cs new file mode 100644 index 0000000..e0bacde --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextBindingSourceFixture.cs @@ -0,0 +1,243 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutContextBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutContextBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_RenderingContextResponseIsNull_ShouldReturnNull( + SitecoreLayoutContextBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(CustomContext))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Response = null; + + // Act + CustomContext? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as CustomContext; + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_RenderingContextResponseContentIsNull_ShouldReturnNull( + SitecoreLayoutContextBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(CustomContext))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Response!.Content = null; + + // Act + CustomContext? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as CustomContext; + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_RenderingContextRawDataIsNull_ShouldReturnNull( + SitecoreLayoutContextBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(CustomContext))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + renderingContext.Response!.Content!.ContextRawData = null!; + + // Act + CustomContext? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as CustomContext; + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithSubContextType_ReturnsModel( + SitecoreLayoutContextBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(CustomContext))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + CustomContext? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as CustomContext; + + // Assert + model.Should().NotBeNull(); + model!.TestClass1.Should().NotBeNull(); + model.TestClass2.Should().NotBeNull(); + model.SingleProperty.Should().NotBeNullOrWhiteSpace(); + model.Site.Should().NotBeNull(); + model.PageState.Should().NotBeNull(); + model.Language.Should().NotBeNullOrWhiteSpace(); + model.WrongName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithIndividualType_ReturnsModel( + SitecoreLayoutContextBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(CustomContextIndividual))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + CustomContextIndividual? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as CustomContextIndividual; + + // Assert + model.Should().NotBeNull(); + model!.TestClass1.Should().NotBeNull(); + model.TestClass2.Should().NotBeNull(); + model.SingleProperty.Should().NotBeNullOrWhiteSpace(); + model.WrongName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithIndividualInnerType_ReturnsModel( + SitecoreLayoutContextBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TestClass1))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + modelBindingContext.FieldName.Returns("TestClass1"); + + // Act + TestClass1? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext) as TestClass1; + + // Assert + model.Should().NotBeNull(); + model!.TestString.Should().NotBeNullOrWhiteSpace(); + model.TestInt.Should().NotBe(default); + model.TestTime.Should().NotBe(default); + } + + // ReSharper disable UnusedAutoPropertyAccessor.Local - Used by serialization + // ReSharper disable UnusedMember.Local - Used by serialization + private class TestClass1 + { + public string? TestString { get; set; } + + public int? TestInt { get; set; } + + public DateTime? TestTime { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local - Init by serialization + private class TestClass2 + { + public string? TestString { get; set; } + + public int TestInt { get; set; } + } + + private class CustomContext : Context + { + public TestClass1? TestClass1 { get; set; } + + public TestClass2? TestClass2 { get; set; } + + public TestClass1? WrongName { get; set; } + + public string? SingleProperty { get; set; } + } + + private class CustomContextIndividual + { + public TestClass1? TestClass1 { get; set; } + + public TestClass2? TestClass2 { get; set; } + + public TestClass1? WrongName { get; set; } + + public string? SingleProperty { get; set; } + } + + // ReSharper restore UnusedMember.Local - Used by serialization + // ReSharper restore UnusedAutoPropertyAccessor.Local - Used by serialization +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextPropertyBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextPropertyBindingSourceFixture.cs new file mode 100644 index 0000000..860bdb4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutContextPropertyBindingSourceFixture.cs @@ -0,0 +1,201 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutContextPropertyBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutContextPropertyBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullContext_ReturnsNull( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + renderingContext.Response!.Content!.Sitecore!.Context = null; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_ReturnsNull( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyPropertyName_ReturnsNull( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_FieldNameIsUsedForBinding( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = "Language"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("en"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_PropertyNameIsSet_PropertyNameIsUsedForBinding( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "Language"; + modelBindingContext.FieldName = "someParameter"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("en"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownPropertyName_ReturnsNull( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidName"; + modelBindingContext.FieldName = "invalidPropName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidPropertyName_ReturnsModel( + SitecoreLayoutContextPropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "Language"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("en"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutResponseBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutResponseBindingSourceFixture.cs new file mode 100644 index 0000000..de83112 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutResponseBindingSourceFixture.cs @@ -0,0 +1,104 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutResponseBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1; + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutRouteBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithLayoutServiceResponseType_ReturnsModel( + SitecoreLayoutResponseBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(SitecoreLayoutResponse))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().Be(renderingContext.Response); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidModel_Null( + SitecoreLayoutResponseBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? result = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + result.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteBindingSourceFixture.cs new file mode 100644 index 0000000..1cba1ef --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteBindingSourceFixture.cs @@ -0,0 +1,149 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutRouteBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutRouteBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithRouteType_ReturnsModel( + SitecoreLayoutRouteBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(Route))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().Be(renderingContext.Response!.Content!.Sitecore!.Route); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithGenericRouteType_ReturnsModel( + SitecoreLayoutRouteBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(Route))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeOfType(typeof(Route)); + model.Should().BeEquivalentTo(renderingContext.Response!.Content!.Sitecore!.Route); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithUnknownType_ReturnsNull( + SitecoreLayoutRouteBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? result = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + result.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullRoute_ReturnsNull( + SitecoreLayoutRouteBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext) + { + // Arrange + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithoutRoute; + SitecoreRenderingContext renderingContext = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldBindingSourceFixture.cs new file mode 100644 index 0000000..b167b3b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldBindingSourceFixture.cs @@ -0,0 +1,240 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutRouteFieldBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutRouteFieldBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullRoute_ReturnsNull( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + renderingContext.Response!.Content!.Sitecore!.Route = null; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullFieldName_ReturnsNull( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyFieldName_ReturnsNull( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullFieldName_BindingContextFieldNameIsUsedForBinding( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = null; + modelBindingContext.FieldName = "pageTitle"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("Styleguide | Sitecore JSS"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_FieldNameIsSet_PropertyNameIsUsedForBinding( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "pageTitle"; + modelBindingContext.FieldName = "someParam"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("Styleguide | Sitecore JSS"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownFieldName_ReturnsNull( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidFieldName"; + modelBindingContext.FieldName = "invalidFieldName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidFieldName_ReturnsModel( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(TextField))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "pageTitle"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(TextField)); + TextField? fieldModel = model as TextField; + fieldModel!.Value.Should().Be("Styleguide | Sitecore JSS"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidType_ReturnsNull( + SitecoreLayoutRouteFieldBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + sut.Name = "pageTitle"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldsBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldsBindingSourceFixture.cs new file mode 100644 index 0000000..3a9351e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRouteFieldsBindingSourceFixture.cs @@ -0,0 +1,135 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutRouteFieldsBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutRouteFieldsBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullRoute_ReturnsNull( + SitecoreLayoutRouteFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + renderingContext.Response!.Content!.Sitecore!.Route = null; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidFieldsModel_ReturnsPopulatedModel( + SitecoreLayoutRouteFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(RouteFields))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().BeOfType(typeof(RouteFields)); + RouteFields? fieldModel = model as RouteFields; + fieldModel!.PageTitle!.Value.Should().Be("Styleguide | Sitecore JSS"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithInvalidFieldsModel_ReturnsNull( + SitecoreLayoutRouteFieldsBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + ModelMetadata? modelMeta = Substitute.For(ModelMetadataIdentity.ForType(typeof(string))); + modelBindingContext.ModelMetadata.Returns(modelMeta); + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + private class RouteFields + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local - Used by serialization + public TextField? PageTitle { get; set; } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRoutePropertyBindingSourceFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRoutePropertyBindingSourceFixture.cs new file mode 100644 index 0000000..c51f408 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Binding/Sources/SitecoreLayoutRoutePropertyBindingSourceFixture.cs @@ -0,0 +1,201 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Binding.Sources; + +public class SitecoreLayoutRoutePropertyBindingSourceFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreLayoutResponseContent content = CannedResponses.StyleGuide1WithContext; + Placeholder? jssPlaceholder = content.Sitecore?.Route?.Placeholders["jss-main"]; + SitecoreRenderingContext context = new() + { + Component = jssPlaceholder != null && jssPlaceholder.Any() ? jssPlaceholder.ComponentAt(0) : null, + Response = new SitecoreLayoutResponse([]) + { + Content = content + } + }; + + f.Inject(context); + }; + + [Theory] + [AutoNSubstituteData] + public void GetModel_IsGuarded( + SitecoreLayoutRoutePropertyBindingSource sut, + ModelBindingContext modelBindingContext, + ISitecoreRenderingContext renderingContext, + IServiceProvider serviceProvider) + { + // Arrange + Func allNull = + () => sut.GetModel(null!, null!, null!); + Func serviceProviderNull = + () => sut.GetModel(null!, modelBindingContext, renderingContext); + Func firstAndSecondArgsNull = + () => sut.GetModel(null!, null!, renderingContext); + Func firstAndThirdArgsNull = + () => sut.GetModel(null!, modelBindingContext, null!); + Func secondAndThirdArgsNull = + () => sut.GetModel(serviceProvider, null!, null!); + Func bindingContextNull = + () => sut.GetModel(serviceProvider, null!, renderingContext); + Func contextNull = + () => sut.GetModel(serviceProvider, modelBindingContext, null!); + + // Act & Assert + allNull.Should().Throw(); + serviceProviderNull.Should().Throw(); + firstAndSecondArgsNull.Should().Throw(); + firstAndThirdArgsNull.Should().Throw(); + secondAndThirdArgsNull.Should().Throw(); + bindingContextNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullRoute_ReturnsNull( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + renderingContext.Response!.Content!.Sitecore!.Route = null; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_ReturnsNull( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = null!; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_EmptyPropertyName_ReturnsNull( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = string.Empty; + modelBindingContext.FieldName = string.Empty; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_NullPropertyName_FieldNameIsUsedForBinding( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = null; + modelBindingContext.FieldName = "DatabaseName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("test-database-name"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_PropertyNameIsSet_PropertyNameIsUsedForBinding( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "DatabaseName"; + modelBindingContext.FieldName = "someParameter"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("test-database-name"); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_UnknownPropertyName_ReturnsNull( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "InvalidName"; + modelBindingContext.FieldName = "invalidPropName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void GetModel_WithValidPropertyName_ReturnsModel( + SitecoreLayoutRoutePropertyBindingSource sut, + IServiceProvider serviceProvider, + ModelBindingContext modelBindingContext, + SitecoreRenderingContext renderingContext) + { + // Arrange + sut.Name = "DatabaseName"; + + // Act + object? model = sut.GetModel(serviceProvider, modelBindingContext, renderingContext); + + // Assert + model.Should().NotBeNull(); + model.Should().Be("test-database-name"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Configuration/RenderingEngineOptionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Configuration/RenderingEngineOptionsFixture.cs new file mode 100644 index 0000000..55d1ca3 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Configuration/RenderingEngineOptionsFixture.cs @@ -0,0 +1,81 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Configuration; + +public class RenderingEngineOptionsFixture +{ + private readonly RenderingEngineOptions _sut = new(); + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Register(s => new PathString("/" + s)); + }; + + [Fact] + public void Ctor_RendererRegistry_IsEmpty() + { + _sut.RendererRegistry.Should().BeEmpty(); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_MapToRequest_SetsPathByDefault(HttpRequest http) + { + // Arrange + SitecoreLayoutRequest scRequest = []; + + // Act + _sut.RequestMappings.Single().Invoke(http, scRequest); + + // Assert + scRequest.Path().Should().Be(http.Path); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_MapToRequest_AcceptsCulture(HttpRequest http) + { + // Arrange + PathString path = http.Path; + http.Path = "/da" + http.Path; + http.RouteValues.Add("culture", "da"); + http.RouteValues.Add(RenderingEngineConstants.RouteValues.SitecoreRoute, path); + + SitecoreLayoutRequest scRequest = []; + + // Act + _sut.RequestMappings.Single().Invoke(http, scRequest); + + // Assert + scRequest.Path().Should().Be(path); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_MapToRequest_AcceptCulture_From_RequestCultureFeature(HttpRequest http) + { + // Arrange + string requestedCulture = "da"; + http.Path = http.Path; + + SitecoreLayoutRequest scRequest = []; + IRequestCultureFeature? requestCultureFeature = Substitute.For(); + requestCultureFeature.RequestCulture.Returns(new RequestCulture(requestedCulture)); + http.HttpContext.Features.Get().Returns(requestCultureFeature); + + // Act + _sut.RequestMappings.Single().Invoke(http, scRequest); + + // Assert + scRequest.Language().Should().Be(requestedCulture); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs new file mode 100644 index 0000000..33c14c9 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ApplicationBuilderExtensionsFixture.cs @@ -0,0 +1,85 @@ +using System.Reflection; +using System.Reflection.PortableExecutable; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Localization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class ApplicationBuilderExtensionsFixture +{ + [Fact] + public void UseSitecoreRenderingEngine_NullApplicationBuilder_Throws() + { + // Arrange + Func act = + () => RenderingEngine.Extensions.ApplicationBuilderExtensions.UseSitecoreRenderingEngine(null!); + + // Act & Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void UseSitecoreRenderingEngine_WithAppBuilderAndRenderingEngineServicesAdded_CallsMiddleware(ServiceCollection services, IApplicationBuilder appBuilder) + { + // Arrange + services.AddSitecoreRenderingEngine(); + appBuilder.ApplicationServices.GetService(typeof(SitecoreQueryStringCultureProvider)) + .Returns(new SitecoreQueryStringCultureProvider()); + IOptions? options = Substitute.For>(); + appBuilder.ApplicationServices.GetService(typeof(IOptions)).Returns(options); + options.Value.Returns(new RequestLocalizationOptions()); + + // Act + appBuilder.UseSitecoreRenderingEngine(); + + // Assert + bool received = appBuilder.ReceivedCalls().Any(c => c.GetArguments().OfType().Any(d => + d.Target?.GetType().GetField("_middleware", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(d.Target).As().FullName == typeof(RenderingEngineMiddleware).FullName)); + received.Should().BeTrue(); + } + + [Theory] + [AutoNSubstituteData] + public void UseSitecoreRenderingEngine_WithAppBuilderAndWithoutRenderingEngineServices_Throws(IApplicationBuilder appBuilder) + { + // Arrange + appBuilder.ApplicationServices.GetService(typeof(RenderingEngineMarkerService)).Returns(null); + Func act = + appBuilder.UseSitecoreRenderingEngine; + + // Act & Assert + act.Should().Throw().WithMessage("The Sitecore Rendering Engine Middleware cannot be enabled without the Rendering Engine Services being registered first. Did you forget to invoke the AddSitecoreRenderingEngine() extension method in Startup.ConfigureServices?"); + } + + [Fact] + public void ApplicationBuilderExtensions_AddRenderingEngineMapping_Guarded() + { + Action act = + () => RenderingEngine.Extensions.ApplicationBuilderExtensions.AddRenderingEngineMapping(null!, (_, _) => { }); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void ApplicationBuilderExtensions_AddRenderingEngineMapping_Without_MappingAction_Guarded(IApplicationBuilder app) + { + // Arrange + app.ApplicationServices.GetService(typeof(IOptions)).Returns(Substitute.For>()); + Action act = + () => app.AddRenderingEngineMapping(null!); + + // Act & Assert + act.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/EncodingExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/EncodingExtensionsFixture.cs new file mode 100644 index 0000000..c3f9f7f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/EncodingExtensionsFixture.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class EncodingExtensionsFixture +{ + [Fact] + public void EncodeControlCharacters_WithEmptyValue_ReturnsEmptyString() + { + // Arrange & Act + string result = string.Empty.EncodeControlCharacters(); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public void EncodeControlCharacters_WithNullValue_ReturnsEmptyString() + { + // Arrange & Act + string result = EncodingExtensions.EncodeControlCharacters(null!); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public void EncodeControlCharacters_WithSpecialCharactersInValue_ReturnsUnencodedValue() + { + // Arrange + const string value = "!\"£$%^&*()_-=@~#><,./?"; + + // Act + string result = value.EncodeControlCharacters(); + + // Assert + result.Should().Be(value); + } + + [Fact] + public void EncodeControlCharacters_WithUnencodedLineBreakInValue_ReturnsValueWithEncodedLineBreak() + { + // Arrange + const string encodedLineBreak = "%0d%0a"; + const string value = "\r\ntest"; + + // Act + string result = value.EncodeControlCharacters(); + + // Assert + result.Should().Be(encodedLineBreak + "test"); + } + + [Fact] + public void EncodeControlCharacters_WithEncodedLineBreakInValue_ReturnsValueWithEncodedLineBreak() + { + // Arrange + const string encodedLineBreak = "%0d%0a"; + const string value = encodedLineBreak + "test"; + + // Act + string result = value.EncodeControlCharacters(); + + // Assert + result.Should().Be(value); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpContextExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpContextExtensionsFixture.cs new file mode 100644 index 0000000..42fc564 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpContextExtensionsFixture.cs @@ -0,0 +1,43 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class HttpContextExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup() + { + return f => + { + f.Inject(null!); + }; + } + + [Fact] + public void GetSitecoreLayoutFeature_Guarded() + { + Func act = + () => HttpContextExtensions.GetSitecoreRenderingContext(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetSitecoreLayoutFeature_ReturnsTheRightFeature(HttpContext context) + { + // act + _ = context.GetSitecoreRenderingContext(); + + // assert + context.Features.Received(1).Get(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpRequestExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpRequestExtensionsFixture.cs new file mode 100644 index 0000000..df3d155 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/HttpRequestExtensionsFixture.cs @@ -0,0 +1,99 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class HttpRequestExtensionsFixture +{ + [Theory] + [AutoNSubstituteData] + public void GetValueFromQueryOrCookies_WithNullKey_Throws(HttpRequest sut) + { + Assert.Throws(() => sut.GetValueFromQueryOrCookies(null!)); + } + + [Theory] + [AutoNSubstituteData] + public void GetValueFromQueryOrCookies_WhenQueryContainsValue_Returns(HttpRequest sut, string key, string value) + { + sut.Query[key].Returns((StringValues)value); + + string? result = sut.GetValueFromQueryOrCookies(key); + + result.Should().Be(value); + } + + [Theory] + [AutoNSubstituteData] + public void GetValueFromQueryOrCookies_WhenCookieContainsValue_Returns(HttpRequest sut, string key, string value) + { + sut.Query[key].Returns(StringValues.Empty); + sut.Cookies[key].Returns(value); + + string? result = sut.GetValueFromQueryOrCookies(key); + + result.Should().Be(value); + } + + [Theory] + [AutoNSubstituteData] + public void GetValueFromQueryOrCookies_WhenQueryOrCookieDonNotContainValue_ReturnsNull(HttpRequest sut, string key) + { + sut.Query[key].Returns(StringValues.Empty); + sut.Cookies[key].Returns(string.Empty); + + string? result = sut.GetValueFromQueryOrCookies(key); + + result.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void TryGetValueFromQueryOrCookies_WithNullKey_Throws(HttpRequest sut) + { + Assert.Throws(() => sut.TryGetValueFromQueryOrCookies(null!, out string? _)); + } + + [Theory] + [AutoNSubstituteData] + public void TryGetValueFromQueryOrCookies_WhenQueryContainsValue_ReturnsTrue(HttpRequest sut, string key, string requestValue) + { + sut.Query[key].Returns((StringValues)requestValue); + + bool result = sut.TryGetValueFromQueryOrCookies(key, out string? value); + + result.Should().BeTrue(); + value.Should().Be(requestValue); + } + + [Theory] + [AutoNSubstituteData] + public void TryGetValueFromQueryOrCookies_WhenCookieContainsValue_ReturnsTrue(HttpRequest sut, string key, string requestValue) + { + sut.Query[key].Returns(StringValues.Empty); + sut.Cookies[key].Returns(requestValue); + + bool result = sut.TryGetValueFromQueryOrCookies(key, out string? value); + + result.Should().BeTrue(); + value.Should().Be(requestValue); + } + + [Theory] + [AutoNSubstituteData] + public void TryGetValueFromQueryOrCookies_WhenQueryOrCookieDonNotContainValue_ReturnsFalse(HttpRequest sut, string key) + { + sut.Query[key].Returns(StringValues.Empty); + sut.Cookies[key].Returns(string.Empty); + + bool result = sut.TryGetValueFromQueryOrCookies(key, out string? value); + + result.Should().BeFalse(); + value.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs new file mode 100644 index 0000000..c6cc69b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs @@ -0,0 +1,72 @@ +using System.Reflection; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class MultisiteAppConfigurationExtensionsFixture +{ + [Fact] + public void AddMultisite_NullServicesProperties_ThrowsExceptions() + { + // Arrange + Func act = + () => MultisiteAppConfigurationExtensions.AddMultisite(null!); + + // Act & Assert + act.Should().Throw().WithParameterName("services"); + } + + [Theory] + [AutoNSubstituteData] + public void AddSitecoreRedirects_RegisterProperServicesAndConfiguration(Action multisiteOptions) + { + // Arrange + IServiceCollection serviceCollection = new ServiceCollection(); + + // Act + serviceCollection.AddMultisite(multisiteOptions); + + // Assert + // NOTE https://stackoverflow.com/questions/57123686/how-to-verify-addsingleton-with-special-type-is-received-using-nsubstitute-frame + serviceCollection.Should().HaveCount(8); + serviceCollection[5].ImplementationInstance.Should().BeEquivalentTo(new ConfigureNamedOptions(string.Empty, multisiteOptions)); + serviceCollection[6].ServiceType.Should().Be(typeof(ISiteCollectionService)); + serviceCollection[6].ImplementationType.Should().Be(typeof(GraphQlSiteCollectionService)); + serviceCollection[7].ServiceType.Should().Be(typeof(ISiteResolver)); + serviceCollection[7].ImplementationType.Should().Be(typeof(SiteResolver)); + } + + [Fact] + public void UseMultisite_NullApplicationBuilder_ThrowsExceptions() + { + Func act = + () => MultisiteAppConfigurationExtensions.UseMultisite(null!); + + act.Should().Throw().WithParameterName("app"); + } + + [Theory] + [AutoNSubstituteData] + public void UseMultisite_UseMultisiteMiddleware(IApplicationBuilder applicationBuilder) + { + // Act + applicationBuilder.ApplicationServices.GetService(typeof(ISiteCollectionService)).Returns(Substitute.For()); + applicationBuilder.ApplicationServices.GetService(typeof(ISiteResolver)).Returns(Substitute.For()); + applicationBuilder.UseMultisite(); + + // Assert + bool received = applicationBuilder.ReceivedCalls().Any(c => c.GetArguments().OfType().Any(d => + d.Target?.GetType().GetField("_middleware", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(d.Target).As().FullName == typeof(MultisiteMiddleware).FullName)); + received.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RazorPageExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RazorPageExtensionsFixture.cs new file mode 100644 index 0000000..bea1c2b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RazorPageExtensionsFixture.cs @@ -0,0 +1,108 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class RazorPageExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Behaviors.Add(new OmitOnRecursionBehavior()); + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + + FeatureCollection features = new() + { + [typeof(ISitecoreRenderingContext)] = new SitecoreRenderingContext + { + Component = new Component(), + Response = new SitecoreLayoutResponse([]) + { + Content = CannedResponses.StyleGuide1, + } + } + }; + + viewContext.HttpContext.Features.Returns(features); + + IRazorPage? page = Substitute.For(); + page.ViewContext.Returns(viewContext); + f.Inject(page); + }; + + [Fact] + public void RazorPageExtensions_SitecoreRoute_Guarded() + { + Func act = + () => RazorPageExtensions.SitecoreRoute(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreRoute__WithRazorPage_ReturnsRoute(IRazorPage page) + { + // Act + Route? route = page.SitecoreRoute(); + + // Assert + route.Should().BeEquivalentTo(CannedResponses.StyleGuide1.Sitecore!.Route); + } + + [Fact] + public void RazorPageExtensions_SitecoreContext_Guarded() + { + Func act = + () => RazorPageExtensions.SitecoreContext(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreContext__WithRazorPage_ReturnsContext(IRazorPage page) + { + // Act + Context? route = page.SitecoreContext(); + + // Assert + route.Should().BeEquivalentTo(CannedResponses.StyleGuide1.Sitecore!.Context); + } + + [Fact] + public void RazorPageExtensions_SitecoreComponent_Guarded() + { + Func act = + () => RazorPageExtensions.SitecoreComponent(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void SitecoreComponent_WithRazorPage_ReturnsComponent(IRazorPage page) + { + // Act + Component? route = page.SitecoreComponent(); + + // Assert + route.Should().BeOfType(typeof(Component)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs new file mode 100644 index 0000000..7e2680b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs @@ -0,0 +1,330 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class RenderingEngineOptionsExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ILogger? logger = Substitute.For>(); + ICustomHtmlHelper? customHtmlHelper = f.Create(); + + IServiceProvider? services = Substitute.For(); + services.GetService(typeof(ILogger)).Returns(logger); + services.GetService(typeof(IHtmlHelper)).Returns(customHtmlHelper); + f.Inject(services); + + f.Inject(new ViewContext()); + ICustomViewComponentHelper? viewComponentHelper = f.Create(); + f.Inject(viewComponentHelper); + f.Inject(viewComponentHelper); + + IServiceProvider? serviceProvider = f.Freeze(); + serviceProvider.GetService(typeof(IViewComponentHelper)).Returns(viewComponentHelper); + + RenderingEngineOptions options = new(); + f.Inject(options); + }; + + [Fact] + public void AddPartialView_NullOptions_Throws() + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddPartialView(null!, (Predicate)null!, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void AddPartialView_InvalidLayoutComponentName_Throws(string value, RenderingEngineOptions options) + { + // Arrange + Action action = () => options.AddPartialView(value, null!); + + // Act / Assert + action.Should().Throw(); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void AddPartialView_EmptyPartialViewPath_Throws(string value, RenderingEngineOptions options, string layoutComponentName) + { + // Arrange + Action action = () => options.AddPartialView(layoutComponentName, value); + + // Act / Assert + action.Should().Throw().WithParameterName("partialViewPath"); + } + + [Theory] + [AutoNSubstituteData] + public void AddPartialView_PartialViewPathIsNotEmpty_MatchesOnPartialFileName( + RenderingEngineOptions options) + { + // Act + options.AddPartialView("~/foo/Bar.cshtml"); + + // Act / Assert + options.RendererRegistry.Values.Should().NotBeEmpty(); + + ComponentRendererDescriptor descriptor = options.RendererRegistry.Values[0]; + descriptor.Match("Bar").Should().BeTrue(); + } + + [Theory] + [AutoNSubstituteData] + public void AddPartialView_PartialViewPathIsNotEmpty_PartialViewComponentRendererIsAddedToOptions( + RenderingEngineOptions options, + IServiceProvider services, + string layoutComponentName, + string partialViewPath) + { + // Act + options.AddPartialView(layoutComponentName, partialViewPath); + + // Act / Assert + options.RendererRegistry.Values.Should().NotBeEmpty(); + + ComponentRendererDescriptor descriptor = options.RendererRegistry.Values[0]; + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(PartialViewComponentRenderer)); + } + + [Fact] + public void AddViewComponent_NullOptions_Throws() + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddViewComponent(null!, (Predicate)null!, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void AddViewComponent_InvalidLayoutComponentName_Throws(string value, RenderingEngineOptions options) + { + // Arrange + Action action = () => options.AddViewComponent(value, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("layoutComponentName"); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(EmptyStrings))] + public void AddViewComponent_EmptyPartialViewPath_Throws(string value, RenderingEngineOptions options, string layoutComponentName) + { + // Arrange + Action action = () => options.AddViewComponent(layoutComponentName, value); + + // Act / Assert + action.Should().Throw(); + } + + [Fact] + public void AddModelBoundViewOfTModel_OptionsIsNull_ThrowsArgumentNullException() + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddModelBoundView(null!, (Predicate)null!, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_PredicateIsNull_ThrowsArgumentNullException(RenderingEngineOptions options) + { + // Arrange + Action action = () => options.AddModelBoundView((Predicate)null!, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("match"); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_ViewComponentNameIsNull_ThrowsArgumentNullException(RenderingEngineOptions options, Predicate match) + { + // Arrange + Action action = () => options.AddModelBoundView(match, null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("viewName"); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_ViewComponentNameIsEmptyString_ThrowsArgumentNullException(RenderingEngineOptions options, Predicate match) + { + // Arrange + Action action = () => options.AddModelBoundView(match, string.Empty); + + // Act / Assert + action.Should().Throw().WithParameterName("viewName"); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_PartialViewPathIsNotEmpty_MatchesOnPartialFileName( + RenderingEngineOptions options) + { + // Act + options.AddModelBoundView("~/foo/Bar.cshtml"); + + // Act / Assert + options.RendererRegistry.Values.Should().NotBeEmpty(); + + ComponentRendererDescriptor descriptor = options.RendererRegistry.Values[0]; + descriptor.Match("Bar").Should().BeTrue(); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_ValidParameters_OptionsRendererRegistryContainsSingleComponentRendererDescriptor(RenderingEngineOptions options, Predicate match, string viewComponentName) + { + // Arrange + options.RendererRegistry.Clear(); + + // Act + options = options.AddModelBoundView(match, viewComponentName); + + // Assert + options.RendererRegistry.Should().ContainSingle(); + options.RendererRegistry[0].Should().BeOfType(typeof(ComponentRendererDescriptor)); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_ValidParametersRendererRegistryIsNotEmpty_NewComponentRendererDescriptorIsAddedToTheEndOfTheList(RenderingEngineOptions options, string viewComponentName) + { + // Arrange + ComponentRendererDescriptor initialDescriptor = new(name => name == "InitialComponent", _ => null!); + options.RendererRegistry.Add(0, initialDescriptor); + + // Act + options = options.AddModelBoundView(name => name == "TestComponent", viewComponentName); + + // Assert + options.RendererRegistry.Should().HaveCount(2); + options.RendererRegistry[1].Should().BeOfType(typeof(ComponentRendererDescriptor)); + } + + [Theory] + [AutoNSubstituteData] + public void AddModelBoundViewOfTModel_ValidParameters_ComponentRendererDescriptorCreatesModelBoundViewComponentComponentRenderer(IServiceProvider services, RenderingEngineOptions options, Predicate match, string viewComponentName) + { + // Arrange + options.RendererRegistry.Clear(); + + // Act + options = options.AddModelBoundView(match, viewComponentName); + + // Assert + ComponentRendererDescriptor descriptor = options.RendererRegistry[0]; + IComponentRenderer renderer = descriptor.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(ModelBoundViewComponentComponentRenderer)); + } + + [Fact] + public void AddDefaultComponentRenderer_NullOptions_Throws() + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddDefaultComponentRenderer(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + } + + [Theory] + [AutoNSubstituteData] + public void AddDefaultComponentRenderer_OptionsIsNotNull_DefaultRendererIsLoggingComponentRenderer(RenderingEngineOptions options, IServiceProvider services) + { + // Act + options.AddDefaultComponentRenderer(); + + // Act / Assert + ComponentRendererDescriptor? descriptor = options.DefaultRenderer; + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor!.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(LoggingComponentRenderer)); + } + + [Fact] + public void AddDefaultComponentRendererOfT_NullOptions_Throws() + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddDefaultComponentRenderer(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + } + + [Theory] + [AutoNSubstituteData] + public void AddDefaultComponentRendererOfT_GenericTypeParameterIsTestComponentRenderer_DefaultRendererIsTestComponentRenderer(RenderingEngineOptions options, IServiceProvider services) + { + // Act + options.AddDefaultComponentRenderer(); + + // Act / Assert + ComponentRendererDescriptor? descriptor = options.DefaultRenderer; + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor!.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(TestComponentRenderer)); + } + + [Theory] + [AutoNSubstituteData] + public void AddPostRenderingAction_NullOptions_Throws(RenderingEngineOptions options) + { + // Arrange + Action action = () => RenderingEngineOptionsExtensions.AddPostRenderingAction(null!, null!); + Action action2 = () => options.AddPostRenderingAction(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("options"); + action2.Should().Throw() + .And.ParamName.Should().Be("postAction"); + } + + private static IEnumerable EmptyStrings() + { + yield return [null!]; + yield return [string.Empty]; + yield return ["\t\t "]; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ServiceCollectionExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ServiceCollectionExtensionsFixture.cs new file mode 100644 index 0000000..874a79d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/ServiceCollectionExtensionsFixture.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Localization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Extensions; + +public class ServiceCollectionExtensionsFixture +{ + [Fact] + public void AddSitecoreRenderingEngine_IsGuarded() + { + Func act = + () => ServiceCollectionExtensions.AddSitecoreRenderingEngine(null!); + + act.Should().Throw(); + } + + [Fact] + public void AddSitecoreRenderingEngine_ServiceCollection_Contains_ExpectedServices() + { + // Arrange + ServiceCollection services = []; + + // Act + services.AddSitecoreRenderingEngine(); + + // Assert + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IHttpContextAccessor) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(ISitecoreLayoutRequestMapper) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IComponentRendererFactory) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IEditableChromeRenderer) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IViewModelBinder) && serviceDescriptor.Lifetime == ServiceLifetime.Scoped); + services.Should().Contain(serviceDescriptor => serviceDescriptor.ServiceType == typeof(SitecoreQueryStringCultureProvider) && serviceDescriptor.Lifetime == ServiceLifetime.Singleton); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Filters/SitecoreLayoutControllerFilterFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Filters/SitecoreLayoutControllerFilterFixture.cs new file mode 100644 index 0000000..282c0a6 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Filters/SitecoreLayoutControllerFilterFixture.cs @@ -0,0 +1,59 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Filters; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Filters; + +public class SitecoreLayoutControllerFilterFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + SitecoreRenderingContext sitecoreLayoutFeature = new() + { + Response = new SitecoreLayoutResponse([]) { Content = CannedResponses.StyleGuide1 } + }; + + FeatureCollection featureCollection = new(); + featureCollection.Set(sitecoreLayoutFeature); + + f.Inject(new BindingInfo()); + f.Inject(new DefaultHttpContext(featureCollection)); + }; + + [Theory] + [AutoNSubstituteData] + public void ASitecoreLayoutControllerFilter_GuardedAsync(SitecoreLayoutContextControllerFilter sut) + { + Action act = + () => sut.OnActionExecuting(null!); + + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void ApplicationBuilderExtensions_ReturnsTheRightFeature( + SitecoreLayoutContextControllerFilter sut, + ActionExecutingContext context, + HttpContext httpContext) + { + // act + sut.OnActionExecuting(context); + + // assert + httpContext.GetSitecoreRenderingContext()!.Controller.Should().Be(context.Controller as ControllerBase); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/MultisiteMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/MultisiteMiddlewareFixture.cs new file mode 100644 index 0000000..5c6916a --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/MultisiteMiddlewareFixture.cs @@ -0,0 +1,114 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Middleware; + +public class MultisiteMiddlewareFixture +{ + private readonly RequestDelegate _next; + private readonly IOptions _renderingEngineOptions; + private readonly ISiteResolver _siteResolver; + private readonly IMemoryCache _memoryCache; + private readonly IOptions _multisiteOptions; + + public MultisiteMiddlewareFixture() + { + _next = Substitute.For(); + _renderingEngineOptions = Substitute.For>(); + _renderingEngineOptions.Value.Returns(new RenderingEngineOptions()); + _siteResolver = Substitute.For(); + _memoryCache = Substitute.For(); + _multisiteOptions = Substitute.For>(); + _multisiteOptions.Value.Returns(new MultisiteOptions()); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Invoke_SetSiteNameFromSiteResolving(HttpRequest httpRequest, string expectedSiteName) + { + // Arrange + httpRequest.HttpContext.Request.Returns(httpRequest); + HttpContext httpContext = httpRequest.HttpContext; + MultisiteMiddleware sut = new(_next, _siteResolver, _memoryCache, _renderingEngineOptions, _multisiteOptions); + _siteResolver.GetByHost(httpRequest.Host.Value).Returns(expectedSiteName); + httpRequest.Query.TryGetValue(Arg.Any(), out _).Returns(false); + + // Act + await sut.Invoke(httpRequest.HttpContext); + httpContext.TryGetResolvedSiteName(out string? resolvedSiteName); + SitecoreLayoutRequest sitecoreLayoutRequest = []; + _renderingEngineOptions.Value.RequestMappings.LastOrDefault()?.Invoke(httpRequest, sitecoreLayoutRequest); + + // Assert + resolvedSiteName.Should().Be(expectedSiteName); + await _next.Received(1).Invoke(httpContext); + sitecoreLayoutRequest.SiteName().Should().Be(expectedSiteName); + await _siteResolver.Received(1).GetByHost(httpRequest.Host.Value); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Invoke_SetSiteNameFromQueryString(HttpRequest httpRequest, string expectedSiteName) + { + // Arrange + httpRequest.HttpContext.Request.Returns(httpRequest); + HttpContext httpContext = httpRequest.HttpContext; + MultisiteMiddleware sut = new(_next, _siteResolver, _memoryCache, _renderingEngineOptions, _multisiteOptions); + + httpRequest.QueryString = new QueryString($"?sc_site={expectedSiteName}"); + + Dictionary queryDictionary = new() + { + { + "sc_site", expectedSiteName + } + }; + httpRequest.Query = new QueryCollection(queryDictionary); + + // Act + await sut.Invoke(httpContext); + httpContext.TryGetResolvedSiteName(out string? resolvedSiteName); + SitecoreLayoutRequest sitecoreLayoutRequest = []; + _renderingEngineOptions.Value.RequestMappings.LastOrDefault()?.Invoke(httpRequest, sitecoreLayoutRequest); + + // Assert + resolvedSiteName.Should().Be(expectedSiteName); + await _next.Received(1).Invoke(httpContext); + sitecoreLayoutRequest.SiteName().Should().Be(expectedSiteName); + await _siteResolver.Received(0).GetByHost(httpRequest.Host.Value); + } + + [Theory] + [AutoNSubstituteData] + internal async Task Invoke_DoesNotSetSiteNameIfItIsNotResolved(HttpContext httpContext, string defaultSiteName) + { + // Arrange + httpContext.Request.HttpContext.Returns(httpContext); + MultisiteMiddleware sut = new(_next, _siteResolver, _memoryCache, _renderingEngineOptions, _multisiteOptions); + httpContext.Request.Query.TryGetValue(Arg.Any(), out _).Returns(false); + + // Act + await sut.Invoke(httpContext); + httpContext.TryGetResolvedSiteName(out string? resolvedSiteName); + SitecoreLayoutRequest sitecoreLayoutRequest = []; + sitecoreLayoutRequest.SiteName(defaultSiteName); + _renderingEngineOptions.Value.RequestMappings.LastOrDefault()?.Invoke(httpContext.Request, sitecoreLayoutRequest); + + // Assert + resolvedSiteName.Should().Be(null); + sitecoreLayoutRequest.SiteName().Should().Be(defaultSiteName); + await _next.Received(1).Invoke(httpContext); + await _siteResolver.Received(1).GetByHost(httpContext.Request.Host.Value); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEngineMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEngineMiddlewareFixture.cs new file mode 100644 index 0000000..3cf2275 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEngineMiddlewareFixture.cs @@ -0,0 +1,115 @@ +using AutoFixture; +using AutoFixture.Idioms; +using AutoFixture.Xunit2; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Middleware; + +public class RenderingEngineMiddlewareFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Behaviors.Add(new OmitOnRecursionBehavior()); + f.Inject(new BindingInfo()); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_Guarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_Guarded( + RenderingEngineMiddleware sut, + IViewComponentHelper componentHelper, + IHtmlHelper htmlHelper) + { + Func act = + () => sut.Invoke(null!, componentHelper, htmlHelper); + + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_NextMiddlewareCalled( + [Frozen] RequestDelegate next, + HttpContext httpContext, + RenderingEngineMiddleware sut, + IViewComponentHelper componentHelper, + IHtmlHelper htmlHelper) + { + // act + await sut.Invoke(httpContext, componentHelper, htmlHelper); + + // assert + Received.InOrder(() => next.Invoke(httpContext)); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_ContextAddedToHttpContext( + [Frozen] ISitecoreLayoutClient layoutClient, + [Frozen] ISitecoreLayoutRequestMapper requestMapper, + HttpContext httpContext, + RenderingEngineMiddleware sut, + IViewComponentHelper componentHelper, + IHtmlHelper htmlHelper) + { + httpContext.Features.Get().Returns((ISitecoreRenderingContext)null!); + + // act + await sut.Invoke(httpContext, componentHelper, htmlHelper); + + // assert + requestMapper.Received(1).Map(httpContext.Request); + Received.InOrder(() => layoutClient.Request(Arg.Any())); + httpContext.Features.Get().Should().NotBeNull(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_ExecutedPostRenderingActions( + [Frozen] IOptions options, + HttpContext httpContext, + RenderingEngineMiddleware sut, + IViewComponentHelper componentHelper, + IHtmlHelper htmlHelper) + { + // arrange + httpContext.Features.Get().Returns((ISitecoreRenderingContext)null!); + + int executed = 0; + + options.Value.AddPostRenderingAction(_ => { executed++; }); + options.Value.AddPostRenderingAction(_ => { executed++; }); + + httpContext.Features.Get>().Returns(options); + + // act + await sut.Invoke(httpContext, componentHelper, htmlHelper); + + // assert + executed.Should().Be(2); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEnginePipelineFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEnginePipelineFixture.cs new file mode 100644 index 0000000..457f01b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Middleware/RenderingEnginePipelineFixture.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Localization; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Middleware; + +public class RenderingEnginePipelineFixture +{ + [Theory] + [AutoNSubstituteData] + public void Configure_NullApp_Throws(RenderingEnginePipeline sut) + { + // Arrange + Action action = () => sut.Configure(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("app"); + } + + [Theory] + [AutoNSubstituteData] + public void Configure_WithApp_RegistersMiddleware(RenderingEnginePipeline sut, IApplicationBuilder app) + { + // Arrange + app.ApplicationServices.GetService(typeof(IOptions)) + .Returns(Substitute.For>()); + app.ApplicationServices.GetService(typeof(SitecoreQueryStringCultureProvider)) + .Returns(new SitecoreQueryStringCultureProvider()); + IOptions? options = Substitute.For>(); + app.ApplicationServices.GetService(typeof(IOptions)).Returns(options); + options.Value.Returns(new RequestLocalizationOptions()); + + // Act + sut.Configure(app); + + // Assert + bool received = app.ReceivedCalls().Any(c => c.GetArguments().OfType().Any(d => + d.Target?.GetType().GetField("_middleware", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(d.Target).As().FullName == typeof(RenderingEngineMiddleware).FullName)); + received.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/BindingViewComponentMock.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/BindingViewComponentMock.cs new file mode 100644 index 0000000..5672ca1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/BindingViewComponentMock.cs @@ -0,0 +1,10 @@ +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.ViewComponents; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class BindingViewComponentMock(IViewModelBinder binder) + : BindingViewComponent(binder) +{ + public IViewModelBinder BinderAccessor => Binder; +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ContentBlock.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ContentBlock.cs new file mode 100644 index 0000000..48ecd53 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ContentBlock.cs @@ -0,0 +1,10 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class ContentBlock +{ + public TextField? Heading { get; set; } + + public RichTextField? Content { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/HeaderBlock.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/HeaderBlock.cs new file mode 100644 index 0000000..1f04235 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/HeaderBlock.cs @@ -0,0 +1,10 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class HeaderBlock +{ + public TextField? Heading1 { get; set; } + + public TextField? Heading2 { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomHtmlHelper.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomHtmlHelper.cs new file mode 100644 index 0000000..b6d6005 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomHtmlHelper.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public interface ICustomHtmlHelper : IHtmlHelper, IViewContextAware; \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomViewComponentHelper.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomViewComponentHelper.cs new file mode 100644 index 0000000..348e886 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/ICustomViewComponentHelper.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public interface ICustomViewComponentHelper : IViewComponentHelper, IViewContextAware; \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestBindingSource.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestBindingSource.cs new file mode 100644 index 0000000..ee57867 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestBindingSource.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class TestBindingSource() + : BindingSource("test", "test", false, false); \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestComponentRenderer.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestComponentRenderer.cs new file mode 100644 index 0000000..d63cf22 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestComponentRenderer.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +internal class TestComponentRenderer : IComponentRenderer +{ + public const string HtmlContent = "Test Component"; + + public const string ChromeContent = """{"commands":[{"click":"chrome:placeholder:addControl","header":"Add to here","icon":"/temp/iconcache/office/16x16/add.png","disabledIcon":"/temp/add_disabled16x16.png","isDivider":false,"tooltip":"Add a new rendering to the '{0}' placeholder.","type":""},{"click":"chrome:placeholder:editSettings","header":"","icon":"/temp/iconcache/office/16x16/window_gear.png","disabledIcon":"/temp/window_gear_disabled16x16.png","isDivider":false,"tooltip":"Edit the placeholder settings.","type":""}],"contextItemUri":"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1","custom":{"allowedRenderings":["1DE91AADC1465D8983FA31A8FD63EBB3","4E3C94B3A9D25478B7548D87283D8AA6","26D9B310A5365D6B975442DB6BE1D381","27EA18D87B6456108919947077956819"],"editable":"true"},"displayName":"Main","expandedDisplayName":null}"""; + + public Task Render(ISitecoreRenderingContext renderingContext, ViewContext viewContext) + { + IHtmlContent htmlContent = new HtmlString(HtmlContent); + return Task.FromResult(htmlContent); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelBinderProviderContext.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelBinderProviderContext.cs new file mode 100644 index 0000000..d4aaacc --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelBinderProviderContext.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class TestModelBinderProviderContext(ModelMetadata modelMetadata) + : ModelBinderProviderContext +{ + public TestModelBinderProviderContext(ModelMetadata modelMetadata, BindingInfo bindingInfo) + : this(modelMetadata) + { + BindingInfo = bindingInfo; + } + + public override BindingInfo BindingInfo { get; } = new(); + + public override ModelMetadata Metadata { get; } = modelMetadata; + + public override IModelMetadataProvider MetadataProvider { get; } = new EmptyModelMetadataProvider(); + + public override IModelBinder CreateBinder(ModelMetadata metadata) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelMetadata.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelMetadata.cs new file mode 100644 index 0000000..99d1db4 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestModelMetadata.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class TestModelMetadata(ModelMetadataIdentity identity, Type type) + : ModelMetadata(identity) +{ + public TestModelMetadata(Type type) + : this(ModelMetadataIdentity.ForType(type), type) + { + } + + public new Type UnderlyingOrModelType { get; } = type; + + public override IReadOnlyDictionary AdditionalValues => null!; + + public override string BinderModelName => string.Empty; + + public override Type BinderType => null!; + + public override BindingSource BindingSource => null!; + + public override bool ConvertEmptyStringToNull => false; + + public override string DataTypeName => string.Empty; + + public override string Description => string.Empty; + + public override string DisplayFormatString => string.Empty; + + public override string DisplayName => string.Empty; + + public override string EditFormatString => string.Empty; + + public override ModelMetadata ElementMetadata => null!; + + public override IEnumerable> EnumGroupedDisplayNamesAndValues => null!; + + public override IReadOnlyDictionary EnumNamesAndValues => null!; + + public override bool HasNonDefaultEditFormat => false; + + public override bool HideSurroundingHtml => false; + + public override bool HtmlEncode => false; + + public override bool IsBindingAllowed => false; + + public override bool IsBindingRequired => false; + + public override bool IsEnum => false; + + public override bool IsFlagsEnum => false; + + public override bool IsReadOnly => false; + + public override bool IsRequired => false; + + public override ModelBindingMessageProvider ModelBindingMessageProvider => null!; + + public override string NullDisplayText => string.Empty; + + public override int Order => 0; + + public override string Placeholder => string.Empty; + + public override ModelPropertyCollection Properties => null!; + + public override IPropertyFilterProvider PropertyFilterProvider => null!; + + public override Func PropertyGetter => null!; + + public override Action? PropertySetter => null; + + public override bool ShowForDisplay => false; + + public override bool ShowForEdit => false; + + public override string SimpleDisplayProperty => string.Empty; + + public override string TemplateHint => string.Empty; + + public override bool ValidateChildren => false; + + public override IReadOnlyList ValidatorMetadata => []; +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestPlaceholderFeature.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestPlaceholderFeature.cs new file mode 100644 index 0000000..cd4beaf --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestPlaceholderFeature.cs @@ -0,0 +1,8 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class TestPlaceholderFeature : IPlaceholderFeature +{ + public string? Content { get; set; } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestSitecoreLayoutBindingSource.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestSitecoreLayoutBindingSource.cs new file mode 100644 index 0000000..adb130d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Mocks/TestSitecoreLayoutBindingSource.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding.Sources; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; + +public class TestSitecoreLayoutBindingSource() + : SitecoreLayoutBindingSource("test", "test", false, false) +{ + public override object GetModel(IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) + { + return null!; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs new file mode 100644 index 0000000..7040565 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs @@ -0,0 +1,50 @@ +using AutoFixture; +using FluentAssertions; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class ComponentRendererDescriptorFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup() + { + return f => + { + IComponentRenderer? renderer = Substitute.For(); + f.Inject(renderer); + + IServiceProvider? services = Substitute.For(); + services.GetService(typeof(IComponentRenderer)).Returns(renderer); + f.Inject(services); + }; + } + + [Fact] + public void Ctor_IsGuarded() + { + // Arrange + Func act = + () => new ComponentRendererDescriptor(null!, null!); + + // Act & Assert + act.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void GetOrCreate_ServiceProviderContainsRendererType_ReturnsRendererType(IServiceProvider services, IComponentRenderer renderer) + { + // Arrange + ComponentRendererDescriptor sut = new(name => name == "Test", _ => renderer); + + // Act + IComponentRenderer result = sut.GetOrCreate(services); + + // Assert + result.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs new file mode 100644 index 0000000..6316877 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs @@ -0,0 +1,106 @@ +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class ComponentRendererFactoryFixture +{ + private const string TestComponentName = "TestComponent"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + IComponentRenderer? componentRenderer = f.Freeze(); + List componentRenderers = [componentRenderer]; + f.Inject>(componentRenderers); + + ServiceCollection services = []; + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + RenderingEngineOptions renderingEngineOptions = new() + { + RendererRegistry = new SortedList + { + { 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer) } + } + }; + + IOptions options = Options.Create(renderingEngineOptions); + ComponentRendererFactory componentRendererFactory = new(options, serviceProvider); + + f.Inject(componentRendererFactory); + + Component component = new() { Name = TestComponentName }; + f.Inject(component); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void GetInstance_NullComponent_Throws(ComponentRendererFactory sut) + { + // Arrange + Component component = null!; + Func act = + () => sut.GetRenderer(component); + + // Act & assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'component')"); + } + + [Theory] + [AutoNSubstituteData] + public void GetInstance_EmptyComponentName_Throws(ComponentRendererFactory sut) + { + // Arrange + Component component = new() { Name = string.Empty }; + Func act = + () => sut.GetRenderer(component); + + // Act & assert + act.Should().Throw().WithParameterName("componentName"); + } + + [Theory] + [AutoNSubstituteData] + public void GetInstance_RendererTypeDoesNotExist_Throws( + ComponentRendererFactory sut) + { + // Arrange + Component component = new() { Name = "testComponent" }; + Func act = + () => sut.GetRenderer(component); + + // Act & assert + act.Should().Throw().WithMessage("The component renderer descriptor for testComponent is null. Please ensure that correct Sitecore component-to-view mappings are defined as part of the AddSitecoreRenderingEngine options in Startup.ConfigureServices."); + } + + [Theory] + [AutoNSubstituteData] + public void GetInstance_RendererExists_ReturnsValidValue( + IComponentRenderer componentRenderer, + Component component, + ComponentRendererFactory sut) + { + // Act + IComponentRenderer result = sut.GetRenderer(component); + + // Assert + result.Should().Be(componentRenderer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/EditableChromeRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/EditableChromeRendererFixture.cs new file mode 100644 index 0000000..994589d --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/EditableChromeRendererFixture.cs @@ -0,0 +1,76 @@ +using System.Text; +using FluentAssertions; +using Microsoft.AspNetCore.Html; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class EditableChromeRendererFixture +{ + [Theory] + [AutoNSubstituteData] + public void Render_IsGuarded( + EditableChromeRenderer sut, + EditableChrome chrome) + { + // Arrange + chrome.Name = string.Empty; + Func actNull = + () => sut.Render(null!); + Func actChrome = + () => sut.Render(chrome); + + // Act / Assert + actNull.Should().Throw(); + actChrome.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void Render_WithValidChromeObject_ReturnsExpectedChromeHtml( + EditableChromeRenderer sut, + EditableChrome chrome) + { + // Act + IHtmlContent result = sut.Render(chrome); + string expectedHtml = BuildChromeHtml(chrome); + + // Assert + result.ToString().Should().Be(expectedHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Render_WithChromeObjectWithoutAttributes_ReturnsExpectedChromeHtml( + EditableChromeRenderer sut, + EditableChrome chrome) + { + // Arrange + chrome.Attributes.Clear(); + + // Act + IHtmlContent result = sut.Render(chrome); + string expectedHtml = BuildChromeHtml(chrome); + + // Assert + result.ToString().Should().Be(expectedHtml); + } + + private static string BuildChromeHtml(EditableChrome chrome) + { + StringBuilder chromeTagString = new($"<{chrome.Name}"); + foreach (string attributeKey in chrome.Attributes.Keys) + { + chromeTagString.Append($" {attributeKey}='{chrome.Attributes[attributeKey]}'"); + } + + chromeTagString.Append(">"); + chromeTagString.Append($"{chrome.Content}"); + chromeTagString.Append($""); + + return chromeTagString.ToString(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/LoggingComponentRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/LoggingComponentRendererFixture.cs new file mode 100644 index 0000000..4c9a385 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/LoggingComponentRendererFixture.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.Design; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class LoggingComponentRendererFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ILogger? logger = Substitute.For>(); + f.Inject(logger); + + f.Inject(new ViewContext()); + + IServiceProvider? serviceProvider = f.Freeze(); + serviceProvider.GetService(typeof(ILogger)).Returns(logger); + + f.Freeze(); + + LoggingComponentRenderer loggingComponentRenderer = new(logger); + f.Inject(loggingComponentRenderer); + }; + + [Fact] + public void Describe_LocatorIsNotNullOrEmpty_DescriptorCanCreateComponentRenderer() + { + // Arrange + ServiceContainer services = new(); + services.AddService(typeof(ILogger), Substitute.For>()); + + // Act + ComponentRendererDescriptor descriptor = LoggingComponentRenderer.Describe(_ => true); + + // Assert + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(LoggingComponentRenderer)); + } + + [Theory] + [AutoNSubstituteData] + public void Render_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyMethod("Render"); + } + + [Theory] + [AutoNSubstituteData] + public void Constructor_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ModelBoundViewComponentComponentRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ModelBoundViewComponentComponentRendererFixture.cs new file mode 100644 index 0000000..2d44b68 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ModelBoundViewComponentComponentRendererFixture.cs @@ -0,0 +1,146 @@ +using AutoFixture; +using AutoFixture.Idioms; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class ModelBoundViewComponentComponentRendererFixture +{ + private const string Locator = "testLocator"; + private const string ViewName = "testView"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Behaviors.Add(new OmitOnRecursionBehavior()); + + IHtmlHelper? htmlHelper = Substitute.For(); + f.Inject(htmlHelper); + + f.Inject(new ViewContext()); + ICustomViewComponentHelper? viewComponentHelper = f.Create(); + f.Inject(viewComponentHelper); + f.Inject(viewComponentHelper); + + IServiceProvider? serviceProvider = f.Freeze(); + serviceProvider.GetService(typeof(IViewComponentHelper)).Returns(viewComponentHelper); + + f.Freeze(); + + ModelBoundViewComponentComponentRenderer viewComponentRenderer = new(Locator, ViewName); + f.Inject(viewComponentRenderer); + }; + + [Theory] + [AutoNSubstituteData] + public void Render_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyMethod("Render"); + } + + [Theory] + [AutoNSubstituteData] + public void Constructor_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Render_RenderingContextComponentIsNull_ViewComponentHelperIsInvokedWithLocatorAndCorrectModelType( + IViewComponentHelper viewComponentHelper, + ISitecoreRenderingContext renderingContext, + ViewContext viewContext, + ModelBoundViewComponentComponentRenderer sut) + { + // Arrange + renderingContext.Component = null; + + // Act + _ = sut.Render(renderingContext, viewContext); + + // Assert + Received.InOrder(() => + _ = viewComponentHelper.InvokeAsync( + Locator, + Arg.Is(m => ConvertToModelType(m) == typeof(HeaderBlock)))); + } + + [Theory] + [AutoNSubstituteData] + public void Render_RenderingContextComponentIsNotNull_ViewComponentHelperIsInvokedWithLocatorAndCorrectModelType( + IViewComponentHelper viewComponentHelper, + ISitecoreRenderingContext renderingContext, + ViewContext viewContext, + ModelBoundViewComponentComponentRenderer sut) + { + // Arrange + renderingContext.Component = new Component(); + + // Act + _ = sut.Render(renderingContext, viewContext); + + // assert + Received.InOrder(() => + { + _ = viewComponentHelper.InvokeAsync( + Locator, + Arg.Is(m => + GetViewName(m) == ViewName && + ConvertToModelType(m) == typeof(HeaderBlock))); + }); + } + + [Theory] + [AutoNSubstituteData] + public void Render_RenderingContextComponentHasFieldsMatchingModelProperties_ViewComponentHelperIsInvokedWithLocatorAndCorrectModelType( + IViewComponentHelper viewComponentHelper, + ISitecoreRenderingContext renderingContext, + ViewContext viewContext, + ModelBoundViewComponentComponentRenderer sut) + { + // Arrange + renderingContext.Component = new Component() + { + Fields = new Dictionary() + { + ["Heading1"] = new Field() { Value = "Test Heading 1" } + } + }; + + // Act + _ = sut.Render(renderingContext, viewContext); + + // assert + Received.InOrder(() => + { + _ = viewComponentHelper.InvokeAsync( + Locator, + Arg.Is(m => + GetViewName(m) == ViewName && + ConvertToModelType(m) == typeof(HeaderBlock))); + }); + } + + private static string? GetViewName(object obj) + { + return obj.GetType().GetProperty("viewName")?.GetValue(obj, null)?.ToString(); + } + + private static Type? ConvertToModelType(object obj) + { + object? modelType = obj.GetType().GetProperty("modelType")?.GetValue(obj, null); + + return modelType as Type; + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/PartialViewComponentRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/PartialViewComponentRendererFixture.cs new file mode 100644 index 0000000..dbaee82 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/PartialViewComponentRendererFixture.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.Design; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class PartialViewComponentRendererFixture +{ + private const string Locator = "testLocator"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Inject(new ViewContext()); + ICustomHtmlHelper? htmlHelper = f.Create(); + f.Inject(htmlHelper); + f.Inject(htmlHelper); + + IServiceProvider? serviceProvider = f.Freeze(); + serviceProvider.GetService(typeof(IHtmlHelper)).Returns(htmlHelper); + + f.Freeze(); + }; + + [Fact] + public void Describe_LocatorIsNotNullOrEmpty_DescriptorCanCreateComponentRenderer() + { + // Arrange + IServiceProvider services = new ServiceContainer(); + + // Act + ComponentRendererDescriptor descriptor = PartialViewComponentRenderer.Describe(_ => true, Locator); + + // Assert + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(PartialViewComponentRenderer)); + } + + [Theory] + [AutoNSubstituteData] + public void Constructor_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Render_PartialView_PassedTheLocator( + IHtmlHelper htmlHelper, + string locator, + ViewContext viewContext) + { + ISitecoreRenderingContext? renderingContext = Substitute.For(); + RenderingHelpers helpers = new(null!, htmlHelper); + renderingContext.RenderingHelpers.Returns(helpers); + PartialViewComponentRenderer sut = new(locator); + + // act + _ = await sut.Render(renderingContext, viewContext); + + // assert + Received.InOrder(() => _ = htmlHelper.PartialAsync(locator, Arg.Any(), null)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/SitecoreRenderingContextFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/SitecoreRenderingContextFixture.cs new file mode 100644 index 0000000..60593d2 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/SitecoreRenderingContextFixture.cs @@ -0,0 +1,28 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class SitecoreRenderingContextFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Inject(new BindingInfo()); + }; + + [Fact] + public void Ctor_SetsDefaults() + { + // Arrange / Act + SitecoreRenderingContext sut = new(); + + // Assert + sut.Response.Should().BeNull(); + sut.Component.Should().BeNull(); + sut.Controller.Should().BeNull(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs new file mode 100644 index 0000000..fc7c687 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs @@ -0,0 +1,87 @@ +using System.ComponentModel.Design; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Rendering; + +public class ViewComponentComponentRendererFixture +{ + private const string Locator = "testLocator"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + IHtmlHelper? htmlHelper = Substitute.For(); + f.Inject(htmlHelper); + + f.Inject(new ViewContext()); + ICustomViewComponentHelper? viewComponentHelper = f.Create(); + f.Inject(viewComponentHelper); + f.Inject(viewComponentHelper); + + IServiceProvider? serviceProvider = f.Freeze(); + serviceProvider.GetService(typeof(IViewComponentHelper)).Returns(viewComponentHelper); + + f.Freeze(); + + ViewComponentComponentRenderer viewComponentRenderer = new(Locator); + f.Inject(viewComponentRenderer); + }; + + [Fact] + public void Describe_LocatorIsNotNullOrEmpty_DescriptorCanCreateComponentRenderer() + { + // Arrange + ServiceContainer services = new(); + + // Act + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(_ => true, Locator); + + // Assert + descriptor.Should().NotBeNull(); + + IComponentRenderer renderer = descriptor.GetOrCreate(services); + renderer.Should().NotBeNull(); + renderer.Should().BeOfType(typeof(ViewComponentComponentRenderer)); + } + + [Theory] + [AutoNSubstituteData] + public void Render_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyMethod("Render"); + } + + [Theory] + [AutoNSubstituteData] + public void Constructor_IsGuarded(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Render_ViewComponent_PassedTheLocator( + IViewComponentHelper viewComponentHelper, + ISitecoreRenderingContext renderingContext, + ViewContext viewContext, + ViewComponentComponentRenderer sut) + { + // act + _ = sut.Render(renderingContext, viewContext); + + // assert + Received.InOrder(() => _ = viewComponentHelper.InvokeAsync(Locator, null)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Routing/LanguageRouteConstraintFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Routing/LanguageRouteConstraintFixture.cs new file mode 100644 index 0000000..7b09ad2 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Routing/LanguageRouteConstraintFixture.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Routing; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.RenderingEngine.Routing; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Routing; + +public class LanguageRouteConstraintFixture +{ + [Theory] + [AutoNSubstituteData] + public void Match_WhenCalled_ShouldHandleNoCulture(LanguageRouteConstraint stu) + { + bool match = stu.Match(null, null, "path", [], RouteDirection.IncomingRequest); + + match.Should().BeFalse(); + } + + [Theory] + [AutoNSubstituteData] + public void Match_WhenCalled_ShouldDetectCulture(LanguageRouteConstraint stu) + { + RouteValueDictionary values = new() + { + { "culture", "da" } + }; + + bool match = stu.Match(null, null, "path", values, RouteDirection.IncomingRequest); + + match.Should().BeTrue(); + } + + [Theory] + [AutoNSubstituteData] + public void Match_WhenCalled_ShouldDetectWrongCulture(LanguageRouteConstraint stu) + { + RouteValueDictionary values = new() + { + { "culture", "css" } + }; + + bool match = stu.Match(null, null, "path", values, RouteDirection.IncomingRequest); + + match.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs new file mode 100644 index 0000000..dbb3763 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs @@ -0,0 +1,125 @@ +using FluentAssertions; +using NSubstitute; +using Sitecore.AspNetCore.SDK.RenderingEngine.Middleware.Models; +using Sitecore.AspNetCore.SDK.RenderingEngine.Services; +using Xunit; + +// ReSharper disable StringLiteralTypo - Casing and spelling must be incorrect for testing +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Services; + +public class SiteResolverFixture +{ + private readonly ISiteCollectionService _siteCollectionService = Substitute.For(); + + [Theory] + [InlineData("like.site.com", "site2")] + [InlineData("longsitehost2.com", "site3")] + [InlineData("ordeR.eu.site.com", "site3")] + [InlineData("i.site.com", "site4")] + public async Task GetByHost_ResolvesByHost_WithMultipleHostNames_AndWhitespaces(string hostName, string expectedSiteName) + { + // Arrange + List siteCollection = + [ + new SiteInfo { Name = "site1", HostName = "*.eu.site.com" }, + new SiteInfo { Name = "site2", HostName = "*.SITE.com | longsitehost.com" }, + new SiteInfo { Name = "site3", HostName = "ordeR.eu.site.com | longsitehost2.com" }, + new SiteInfo { Name = "site4", HostName = "i.site.com" }, + ]; + + _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); + SiteResolver siteResolver = new(_siteCollectionService); + + // Act + string? result = await siteResolver.GetByHost(hostName); + + // Assert + result.Should().Be(expectedSiteName); + } + + [Fact] + public async Task GetByHost_ShouldReturnSiteWhenWildcardIsProvided() + { + // Arrange + List siteCollection = + [ + new SiteInfo { Name = "bar", HostName = "bar.net" }, + new SiteInfo { Name = "wildcard", HostName = "*" } + ]; + + _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); + SiteResolver siteResolver = new(_siteCollectionService); + + // Act + string? result = await siteResolver.GetByHost("foo.com"); + + // Assert + result.Should().Be("wildcard"); + } + + [Fact] + public async Task GetByHost_ShouldPreferMostSpecificMatch() + { + // Arrange + List siteCollection = + [ + new SiteInfo { Name = "foo", HostName = "*" }, + new SiteInfo { Name = "bar", HostName = "*.app.net" }, + new SiteInfo { Name = "i-bar", HostName = "i.app.net" }, + new SiteInfo { Name = "baz", HostName = "baz.app.net" } + ]; + + _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); + SiteResolver siteResolver = new(_siteCollectionService); + + // Act + string? site1 = await siteResolver.GetByHost("foo.net"); + string? site2 = await siteResolver.GetByHost("bar.app.net"); + string? site3 = await siteResolver.GetByHost("i.app.net"); + string? site4 = await siteResolver.GetByHost("Baz.app.net"); + + // Assert + site1.Should().Be("foo"); + site2.Should().Be("bar"); + site3.Should().Be("i-bar"); + site4.Should().Be("baz"); + } + + [Fact] + public async Task GetByHost_ShouldPreferFirstSiteMatchForSameHostName() + { + // Arrange + List siteCollection = + [ + new SiteInfo { Name = "foo", HostName = "*" }, + new SiteInfo { Name = "bar", HostName = "Bar.net" }, + new SiteInfo { Name = "foo-never", HostName = "*" }, + new SiteInfo { Name = "bar-never", HostName = "bar.net" } + ]; + + _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); + SiteResolver siteResolver = new(_siteCollectionService); + + // Act + string? site1 = await siteResolver.GetByHost("foo.net"); + string? site2 = await siteResolver.GetByHost("bar.net"); + + // Assert + site1.Should().Be("foo"); + site2.Should().Be("bar"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task GetByHost_ThrowsArgumentException_IfHostIsNullOrEmpty(string? hostName) + { + // Arrange + SiteResolver siteResolver = new(_siteCollectionService); + Func> act = + () => siteResolver.GetByHost(hostName!); + + // Assert + await act.Should().ThrowAsync().WithParameterName(nameof(hostName)); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests.csproj new file mode 100644 index 0000000..473229f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/SitecoreLayoutRequestMapperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/SitecoreLayoutRequestMapperFixture.cs new file mode 100644 index 0000000..e1071ab --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/SitecoreLayoutRequestMapperFixture.cs @@ -0,0 +1,88 @@ +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Mappers; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests; + +public class SitecoreLayoutRequestMapperFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + f.Register(s => new PathString("/" + s)); + IOptions? optionSub = f.Freeze>(); + RenderingEngineOptions options = new(); + optionSub.Value.Returns(options); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Ctor_NullRequestMappings_Throws(IOptions options) + { + // Arrange + options.Value.RequestMappings = null!; + Action action = () => _ = new SitecoreLayoutRequestMapper(options); + + // Act / Assert + action.Should().Throw() + .WithMessage("A non-nullable value must be provided for the RequestMappings action list."); + } + + [Theory] + [AutoNSubstituteData] + public void Map_WithNullHttpRequest_ThrowsException(SitecoreLayoutRequestMapper sut) + { + // Arrange + Func act = + () => sut.Map(null!); + + // Act & Assert + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'request')"); + } + + [Theory] + [AutoNSubstituteData] + public void Map_NullRequest_ThrowsException(SitecoreLayoutRequestMapper sut) + { + // Arrange + Action action = () => sut.Map(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("request"); + } + + [Theory] + [AutoNSubstituteData] + public void Map_WithRequest_ReturnsMappedRequest( + IOptions options, + HttpRequest request) + { + // Arrange + options.Value.MapToRequest((_, sc) => sc.Path("SET")); + SitecoreLayoutRequestMapper sut = new(options); + + // Act + SitecoreLayoutRequest result = sut.Map(request); + + // Assert + result.Path().Should().Be("SET"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/ComponentHolderFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/ComponentHolderFixture.cs new file mode 100644 index 0000000..15b4a6f --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/ComponentHolderFixture.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using NSubstitute; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers; + +public class ComponentHolderFixture +{ + [Fact] + public void ChangeComponentInCtor() + { + // Arrange + Component testComponent = new(); + ISitecoreRenderingContext? context = Substitute.For(); + context.Component.Returns(new Component()); + + // Act + _ = new ComponentHolder(context, testComponent); + + // Arrange + context.Component.Should().Be(testComponent); + } + + [Fact] + public void ReturnComponentInDispose() + { + // Arrange + Component testComponent = new(); + ISitecoreRenderingContext? context = Substitute.For(); + context.Component.Returns(testComponent); + + // Act + ComponentHolder holder = new(context, new Component()); + holder.Dispose(); + + // Arrange + context.Component.Should().Be(testComponent); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs new file mode 100644 index 0000000..e0ea415 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs @@ -0,0 +1,347 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class DateTagHelperFixture +{ + private const string Editable = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit: editdatetime\\\"})\",\"header\":\"Show calendar\",\"icon\":\"/temp/iconcache/business/16x16/calendar.png\",\"disabledIcon\":\"/temp/calendar_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Shows the calendar\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit: cleardate\\\"})\",\"header\":\"Clear calender\",\"icon\":\"/temp/iconcache/applications/16x16/delete2.png\",\"disabledIcon\":\"/temp/delete2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clear date and time\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit: open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit: personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{EB7384E1-232B-5767-9FFE-4596EED2BD5A}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"dateTime\",\"expandedDisplayName\":null}3/14/2018 5:00:00 PM"; + + private readonly DateTime _date = DateTime.Parse("2012-05-04T00:00:00Z", CultureInfo.InvariantCulture); + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new([], new Dictionary(), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new(string.Empty, [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new DateTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScDateTagWithNullForAttribute_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithValidForAttribute_GeneratesCorrectOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.For = GetModelExpression(new DateField(_date)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithEmptyValueInForAttribute_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.For = GetModelExpression(new DateField()); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithCustomFormat_GeneratesCustomDateFormatOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + const string dateFormat = "MM/dd/yyyy H:mm"; + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.DateFormat = dateFormat; + sut.For = GetModelExpression(new DateField(_date)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(dateFormat, CultureInfo.InvariantCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithCustomCulture_GeneratesCustomDateFormatOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + const string culture = "ua-ua"; + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.Culture = culture; + sut.For = GetModelExpression(new DateField(_date)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CreateSpecificCulture(culture))); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AspForWorksForDateForRandomTags_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "span"; + sut.For = GetModelExpression(new DateField(_date)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + DateField testField = new(_date) + { + EditableMarkup = Editable + }; + sut.DateModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + #region asp-date attribute + [Theory] + [AutoNSubstituteData] + public async Task Process_ScDateTagWithAspDataAttributeWithNullForAttribute_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.DateModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithAspDataAttributeWithValidForAttribute_GeneratesCorrectOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.DateModel = new DateField(_date); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithAspDataAttributeWithEmptyValueInForAttribute_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.DateModel = new DateField(); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateTagWithAspDataAttributeWithCustomFormat_GeneratesCustomDateFormatOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + string dateFormat = "MM/dd/yyyy H:mm"; + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.DateFormat = dateFormat; + sut.DateModel = new DateField(_date); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(dateFormat, CultureInfo.InvariantCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateWithAspDataAttributeTagWithCustomCulture_GeneratesCustomDateFormatOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + string culture = "ua-ua"; + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.Culture = culture; + sut.DateModel = new DateField(_date); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CreateSpecificCulture(culture))); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AspDateWorksForDateForRandomTags_GeneratesEmptyOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "span"; + sut.DateModel = new DateField(_date); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(_date.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScDateWithAspDataAttributeTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + DateTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + DateField testField = new(_date) + { + EditableMarkup = Editable + }; + sut.DateModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + #endregion + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/FileTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/FileTagHelperFixture.cs new file mode 100644 index 0000000..6900ae8 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/FileTagHelperFixture.cs @@ -0,0 +1,184 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class FileTagHelperFixture +{ + private const string FileLinkTag = "a"; + + private const string HrefAttribute = "href"; + + private const string TypeAttribute = "type"; + + private const string TitleAttribute = "title"; + + private const string Target = "target"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new([], new Dictionary(), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new("a", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new FileTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + FileTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScFileTagWithNullForAttribute_GeneratesEmptyOutput( + FileTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScFileTagWithNullDateModelAttribute_GeneratesEmptyOutput( + FileTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag; + sut.For = null!; + sut.FileModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [InlineAutoNSubstituteData("a")] + [InlineAutoNSubstituteData(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag)] + public void Process_ScFileTagWithFileModel_AddsTypeAndTitleAttributeFromModel(string tagName, string target, FileField fileField, FileTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = tagName; + sut.FileModel = fileField; + tagHelperOutput.Attributes.Add(Target, target); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be(FileLinkTag); + tagHelperOutput.Attributes.Should().HaveCount(4); + tagHelperOutput.Attributes.TryGetAttribute(TypeAttribute, out TagHelperAttribute? typeAttribute).Should().BeTrue(); + typeAttribute.Value.Should().Be(fileField.Value.MimeType); + tagHelperOutput.Attributes.TryGetAttribute(TitleAttribute, out TagHelperAttribute? descriptionAttribute).Should().BeTrue(); + descriptionAttribute.Value.Should().Be(fileField.Value.Description); + tagHelperOutput.Attributes.TryGetAttribute(HrefAttribute, out TagHelperAttribute? hrefAttribute).Should().BeTrue(); + hrefAttribute.Value.Should().Be(fileField.Value.Src); + tagHelperOutput.Attributes.TryGetAttribute(Target, out TagHelperAttribute? targetAttribute).Should().BeTrue(); + targetAttribute.Value.Should().Be(target); + tagHelperOutput.Content.GetContent().Should().Be(fileField.Value.Title); + } + + [Theory] + [InlineAutoNSubstituteData("a")] + [InlineAutoNSubstituteData(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag)] + public void Process_ScFileTagWithFileModel_AddsTypeAndTitleAttributeFromTagAttributes(string tagName, string target, string titleFromTag, string customAttributeValue, FileField fileField, FileTagHelper sut, TagHelperContext tagHelperContext) + { + // Arrange + sut.FileModel = fileField; + TagHelperOutput tagHelperOutput = + new( + tagName, + [new TagHelperAttribute(Target, target), new TagHelperAttribute("custom-attribute", customAttributeValue)], + (_, _) => Task.FromResult(new DefaultTagHelperContent().AppendHtml(titleFromTag))); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.TryGetAttribute(Target, out TagHelperAttribute? targetAttribute).Should().BeTrue(); + targetAttribute.Value.Should().Be(target); + tagHelperOutput.Attributes.TryGetAttribute("custom-attribute", out TagHelperAttribute? customAttribute).Should().BeTrue(); + customAttribute.Value.Should().Be(customAttributeValue); + tagHelperOutput.Content.GetContent().Should().Be(titleFromTag); + } + + [Theory] + [InlineAutoNSubstituteData("a")] + [InlineAutoNSubstituteData(RenderingEngineConstants.SitecoreTagHelpers.FileHtmlTag)] + public void Process_ScFileTagWithFileModel_TakesAttributesFromTag(string tagName, string hrefTagValue, string titleTagValue, string typeTagValue, FileField fileField, FileTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = tagName; + sut.FileModel = fileField; + tagHelperOutput.Attributes.Add(HrefAttribute, hrefTagValue); + tagHelperOutput.Attributes.Add(TitleAttribute, titleTagValue); + tagHelperOutput.Attributes.Add(TypeAttribute, typeTagValue); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be(FileLinkTag); + tagHelperOutput.Attributes.TryGetAttribute(TypeAttribute, out TagHelperAttribute? typeAttribute).Should().BeTrue(); + typeAttribute.Value.Should().Be(typeTagValue); + typeAttribute.Value.Should().NotBe(fileField.Value.MimeType); + tagHelperOutput.Attributes.TryGetAttribute(TitleAttribute, out TagHelperAttribute? descriptionAttribute).Should().BeTrue(); + descriptionAttribute.Value.Should().Be(titleTagValue); + typeAttribute.Value.Should().NotBe(fileField.Value.Description); + tagHelperOutput.Attributes.TryGetAttribute(HrefAttribute, out TagHelperAttribute? hrefAttribute).Should().BeTrue(); + hrefAttribute.Value.Should().Be(hrefTagValue); + typeAttribute.Value.Should().NotBe(fileField.Value.Src); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs new file mode 100644 index 0000000..f4c681e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs @@ -0,0 +1,721 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class ImageTagHelperFixture +{ + private const string Html = "\"Sitecore"; + private const string EditableHtml = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit:editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{21218477-59D9-50C4-B7F4-FBCD69760250}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample1\",\"expandedDisplayName\":null}\"Sitecore"; + private const string EditableHtmlWithCustomAttributes = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit:editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{21218477-59D9-50C4-B7F4-FBCD69760250}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample1\",\"expandedDisplayName\":null}\"testAlt\""; + private readonly Image _image = new() { Src = "http://styleguide/-/media/styleguide/data/media/img/sc_logo.png?iar=0&hash=F313AD90AE547CAB09277E42509E289B", Alt = "Sitecore Logo", Width = 100, Height = 100, VSpace = 10, HSpace = 10, Border = 1, Class = "test", Title = "title" }; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new([], new Dictionary(), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new("a", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new ImageTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScImgTagWithNullForAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithValidForAttribute_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = GetModelExpression(new ImageField(_image)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithValidForAttribute_GeneratesOutputWithoutOuterScTextTag( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = GetModelExpression(new ImageField(_image)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithInvalidForAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = GetModelExpression(new RichTextField()); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + tagHelperOutput.Content.GetContent().Should().NotContain(RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEmptyValueInForAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = GetModelExpression(new ImageField(new Image())); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/media/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEmptyEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(_image) + { + EditableMarkup = string.Empty + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(_image) + { + EditableMarkup = EditableHtml + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithParamsAttribute_GeneratesProperImageUrl( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.For = GetModelExpression(new ImageField(_image)); + sut.ImageParams = new { mw = 100, mh = 50 }; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain("?mw=100&mh=50"); + tagHelperOutput.Content.GetContent().Should().NotContain("iar=0"); + tagHelperOutput.Content.GetContent().Should().NotContain("hash=F313AD90AE547CAB09277E42509E289B"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ImgTagWithNullForAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagWithValidForAttribute_GeneratesOutputWithOuterDivTag( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.For = GetModelExpression(new ImageField(_image)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be("img"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagWithValidForAttribute_AddsSrcAndAltAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.For = GetModelExpression(new ImageField(_image)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "src"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "alt"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/media/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagWithParamsAttribute_GeneratesProperImageUrl( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.For = GetModelExpression(new ImageField(_image)); + sut.ImageParams = new { mw = 100, mh = 50 }; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + string? url = tagHelperOutput.Attributes["src"].Value.ToString(); + + // Assert + url.Should().Contain("?mw=100&mh=50"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutputWithCustomAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/jssmedia/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.ImageParams = new { mw = 100, mh = 50 }; + sut.For = GetModelExpression(testField); + + tagHelperOutput.Attributes.Add("class", "testClass"); + tagHelperOutput.Attributes.Add("alt", "testAlt"); + tagHelperOutput.Attributes.Add("width", "50"); + tagHelperOutput.Attributes.Add("height", "50"); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtmlWithCustomAttributes); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagWithAllAttributes_AddsAllAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.For = GetModelExpression(new ImageField(_image)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "src"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "alt"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "width"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "height"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "vspace"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "hspace"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "border"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "title"); + } + + #region asp-image attribute + [Theory] + [AutoNSubstituteData] + public async Task Process_ScImgTagWithNullAspImageAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.ImageModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithValidAspImageAttribute_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.ImageModel = new ImageField(_image); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithValidAspImageAttribute_GeneratesOutputWithoutOuterScTextTag( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.ImageModel = new ImageField(_image); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithEmptyValueInAspImageAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.ImageModel = new ImageField(new Image()); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithAspImageAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/media/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.ImageModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagWithAspImageAttributeEmptyEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(_image) + { + EditableMarkup = string.Empty + }; + sut.ImageModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagAspImageAttributeWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(_image) + { + EditableMarkup = EditableHtml + }; + sut.ImageModel = testField; + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagAspImageAttributeWithParamsAttribute_GeneratesProperImageUrl( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + sut.ImageModel = new ImageField(_image); + sut.ImageParams = new { mw = 100, mh = 50 }; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain("mw=100&mh=50"); + tagHelperOutput.Content.GetContent().Should().NotContain("iar=0"); + tagHelperOutput.Content.GetContent().Should().NotContain("hash=F313AD90AE547CAB09277E42509E289B"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ImgTagAspImageAttributeWithNullForAttribute_GeneratesEmptyOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.ImageModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagAspImageAttributeWithValidForAttribute_GeneratesOutputWithOuterDivTag( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.ImageModel = new ImageField(_image); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be("img"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagAspImageAttributeWithValidForAttribute_AddsSrcAndAltAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.ImageModel = new ImageField(_image); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "src"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "alt"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagAspImageAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/media/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.ImageModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagAspImageAttributeWithParamsAttribute_GeneratesProperImageUrl( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.ImageModel = new ImageField(_image); + sut.ImageParams = new { mw = 100, mh = 50 }; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + string? url = tagHelperOutput.Attributes["src"].Value.ToString(); + + // Assert + url.Should().Contain("?mw=100&mh=50"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScImgTagAspImageAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutputWithCustomAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/jssmedia/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + EditableMarkup = EditableHtml + }; + sut.ImageParams = new { mw = 100, mh = 50 }; + sut.ImageModel = testField; + + tagHelperOutput.Attributes.Add("class", "testClass"); + tagHelperOutput.Attributes.Add("alt", "testAlt"); + tagHelperOutput.Attributes.Add("width", "50"); + tagHelperOutput.Attributes.Add("height", "50"); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(EditableHtmlWithCustomAttributes); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ImgTagAspImageAttributeWithAllAttributes_AddsAllAttributes( + ImageTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "img"; + sut.ImageModel = new ImageField(_image); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "src"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "alt"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "width"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "height"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "vspace"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "hspace"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "border"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "title"); + } + #endregion + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs new file mode 100644 index 0000000..49cb967 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs @@ -0,0 +1,795 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class LinkTagHelperFixture +{ + private const string Editable = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit:editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4252A3F2-57F6-4F29-85F2-C6B14E3EF5A0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"GenealLink\",\"expandedDisplayName\":null}This is description"; + private const string EditableFirstPart = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit:editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4252A3F2-57F6-4F29-85F2-C6B14E3EF5A0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"GenealLink\",\"expandedDisplayName\":null}This is description"; + private const string EditableLastPart = ""; + private const string Html = "This is description"; + private const string HtmlWithRel = "This is description"; + private const string HtmlWithAnchor = "This is description"; + private readonly HyperLink _hyperLink = new() { Class = "sample-css-class", Href = "/demo", Target = string.Empty, Text = "This is description", Title = "Sample alternate text" }; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new([], new Dictionary(), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new("a", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new LinkTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScLinkTagWithNullForAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithValidForAttribute_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithEmptyValueInForAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.For = GetModelExpression(new HyperLinkField(new HyperLink())); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithBlankTargetAttribute_AddsRelAttribute( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink); + testField.Value.Target = "_blank"; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(HtmlWithRel); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAnchor_AddsAnchorToHrefAttribute( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink); + testField.Value.Anchor = "anchor"; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(HtmlWithAnchor); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkWithEditableFalse_RenderAuthorTextInEE( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + string customLinkText) + { + // Arrange + DefaultTagHelperContent defaultTagHelperContent = new(); + defaultTagHelperContent.Append(customLinkText); + TagHelperOutput tagHelperOutput = new(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, [], (_, _) => Task.FromResult(defaultTagHelperContent)); + + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(customLinkText); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkWithEditableTrue_RenderCustomTextInEE( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + string customLinkText) + { + // Arrange + DefaultTagHelperContent defaultTagHelperContent = new(); + defaultTagHelperContent.Append(customLinkText); + TagHelperOutput tagHelperOutput = new(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, [], (_, _) => Task.FromResult(defaultTagHelperContent)); + + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.For = GetModelExpression(testField); + sut.Editable = true; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(_hyperLink.Text); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithEmptyEditableFields_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = string.Empty, + EditableMarkupLast = string.Empty + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_AnchorTagWithNullForAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithValidForAttribute_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "class"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "href"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "title"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithUserAttributes_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + tagHelperOutput.Attributes.Add("class", "test-class"); + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name.Equals("class") && a.Value.Equals("test-class")); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithValidForAttribute_GeneratesOutputWithOuterDivTag( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be("a"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithWithFieldDescription_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(_hyperLink.Text); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagInnerContent_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + string testDescription) + { + // Arrange + tagHelperOutput.TagName = "a"; + tagHelperOutput.Content.Append(testDescription); + sut.For = GetModelExpression(new HyperLinkField(_hyperLink)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(testDescription); + } + + #region asp-link attribute + [Theory] + [AutoNSubstituteData] + public async Task Process_ScLinkTagWithAspLinkAttributeWithNullForAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.LinkModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithAspLinkAttributeWithValidForAttribute_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.LinkModel = new HyperLinkField(_hyperLink); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAspLinkAttributeWithEmptyValueInForAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + sut.LinkModel = new HyperLinkField(new HyperLink()); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAspLinkAttributeWithBlankTargetAttribute_AddsRelAttribute( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink); + testField.Value.Target = "_blank"; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(HtmlWithRel); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAspLinkAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkWithAspLinkAttributeWithEditableFalse_RenderAuthorTextInEE( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + string customLinkText) + { + // Arrange + DefaultTagHelperContent defaultTagHelperContent = new(); + defaultTagHelperContent.Append(customLinkText); + TagHelperOutput tagHelperOutput = new(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, [], (_, _) => Task.FromResult(defaultTagHelperContent)); + + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.LinkModel = testField; + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(customLinkText); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkWithAspLinkAttributeWithEditableTrue_RenderCustomTextInEE( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + string customLinkText) + { + // Arrange + DefaultTagHelperContent defaultTagHelperContent = new(); + defaultTagHelperContent.Append(customLinkText); + TagHelperOutput tagHelperOutput = new(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, [], (_, _) => Task.FromResult(defaultTagHelperContent)); + + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.LinkModel = testField; + sut.Editable = true; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(_hyperLink.Text); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAspLinkAttributeWithEmptyEditableFields_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = string.Empty, + EditableMarkupLast = string.Empty + }; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScLinkTagWithAspLinkAttributeWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.LinkModel = testField; + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Html); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_AnchorTagWithNullAspDateAttribute_GeneratesEmptyOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.LinkModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithValidAspLinkAttribute_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.LinkModel = new HyperLinkField(_hyperLink); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "class"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "href"); + tagHelperOutput.Attributes.Should().Contain(a => a.Name == "title"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithAspLinkAttributeWithUserAttributes_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + tagHelperOutput.Attributes.Add("class", "test-class"); + sut.LinkModel = new HyperLinkField(_hyperLink); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name.Equals("class") && a.Value.Equals("test-class")); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithAspLinkAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + HyperLinkField testField = new(_hyperLink) + { + EditableMarkupFirst = EditableFirstPart, + EditableMarkupLast = EditableLastPart + }; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithAspLinkAttributeWithWithFieldDescription_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + sut.LinkModel = new HyperLinkField(_hyperLink); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(_hyperLink.Text); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithAspLinkAttributeInnerContent_GeneratesCorrectOutput( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + string testDescription) + { + // Arrange + tagHelperOutput.TagName = "a"; + tagHelperOutput.Content.Append(testDescription); + sut.LinkModel = new HyperLinkField(_hyperLink); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(testDescription); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorTagWithAspLinkAttributeWithBlankTargetAttribute_AddsRelAttribute( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + HyperLinkField testField = new(_hyperLink); + testField.Value.Target = "_blank"; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes.Should().Contain(a => a.Name.Equals("target") && a.Value.Equals(_hyperLink.Target)); + tagHelperOutput.Attributes.Should().Contain(a => a.Name.Equals("rel") && a.Value.Equals("noopener noreferrer")); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AnchorLinkTagWithAnchor_AddsAnchorToHrefAttribute( + LinkTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "a"; + HyperLinkField testField = new(_hyperLink); + testField.Value.Anchor = "anchor"; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Attributes["href"].Value.ToString().Should().EndWith($"#{_hyperLink.Anchor}"); + } + #endregion + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs new file mode 100644 index 0000000..3d153d8 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs @@ -0,0 +1,311 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class NumberTagHelperFixture +{ + private const double Number1 = 9.99; + + private const double Number2 = 5.40; + + private const string Editable = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit: editnumber\\\"})\",\"header\":\"Edit number\",\"icon\":\"/temp/iconcache/wordprocessing/16x16/word_count.png\",\"disabledIcon\":\"/temp/word_count_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit number\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit: open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit: personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit: editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{30B7EF86-9214-58E8-9072-6B0CEFE157CD}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}9.99"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new([], new Dictionary(), Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new(string.Empty, [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new NumberTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Process_ScNumberTagWithNullForAttribute_GeneratesEmptyOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.For = null!; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithValidForAttribute_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.For = GetModelExpression(new NumberField(Number1)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithCustomFormat_GeneratesCustomDateFormatOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.NumberFormat = "C"; + sut.For = GetModelExpression(new NumberField(Number1)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(sut.NumberFormat, CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AspForWorksForNumberForRandomTags_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "span"; + sut.For = GetModelExpression(new NumberField(Number2)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number2.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + NumberField testField = new(Number2) + { + EditableMarkup = Editable + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithCultureInfo_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.NumberFormat = "C"; + sut.Culture = "ua-ua"; + sut.For = GetModelExpression(new NumberField(Number1)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(sut.NumberFormat, CultureInfo.CreateSpecificCulture(sut.Culture))); + } + + #region asp-number attribute + [Theory] + [AutoNSubstituteData] + public async Task Process_ScNumberTagWithAspNumberAttributeWithNullForAttribute_GeneratesEmptyOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + sut.NumberModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithAspNumberAttributeWithValidForAttribute_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.NumberModel = new NumberField(Number1); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithAspNumberAttributeWithCustomFormat_GeneratesCustomDateFormatOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.NumberFormat = "C"; + sut.NumberModel = new NumberField(Number1); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(sut.NumberFormat, CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_AspNumberWorksForNumberForRandomTags_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "span"; + sut.NumberModel = new NumberField(Number2); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number2.ToString(CultureInfo.CurrentCulture)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberTagWithAspNumberAttributeWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + NumberField testField = new(Number2) + { + EditableMarkup = Editable + }; + sut.NumberModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Editable); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScNumberWithAspNumberAttributeTagWithCultureInfo_GeneratesCorrectOutput( + NumberTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + sut.NumberFormat = "C"; + sut.Culture = "ua-ua"; + sut.NumberModel = new NumberField(Number1); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(Number1.ToString(sut.NumberFormat, CultureInfo.CreateSpecificCulture(sut.Culture))); + } + #endregion + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs new file mode 100644 index 0000000..85fc8ca --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs @@ -0,0 +1,717 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class RichTextTagHelperFixture +{ + private const string TestHtml = "

This is the test text

"; + + private const string TestEditableMarkup = "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n
"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new( + [], + new Dictionary(), + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new(string.Empty, [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + }); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + #region sc-text tag with asp-for attribute tests + [Theory] + [AutoNSubstituteData] + public async Task Process_ScTextTagWithNullForAndTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.For = null; + sut.TextModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithValidForAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.For = GetModelExpression(new RichTextField(TestHtml, false)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithValidForAttribute_GeneratesOutputWithoutOuterScTextTag( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.For = GetModelExpression(new RichTextField(TestHtml, false)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithInvalidForAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.For = GetModelExpression(new TextField(TestHtml)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + tagHelperOutput.Content.GetContent().Should().NotContain(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEmptyValueInForAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.For = GetModelExpression(new TextField(string.Empty)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestEditableMarkup); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEmptyEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = string.Empty + }; + sut.For = GetModelExpression(testField); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + #endregion + + #region sc-text tag with rich-text attribute tests + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithValidTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.TextModel = new RichTextField(TestHtml, false); + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithValidTextAttribute_GeneratesOutputWithoutOuterScTextTag( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.TextModel = new RichTextField(TestHtml, false); + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().BeNull(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithInvalidTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.TextModel = new TextField(TestHtml); + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + tagHelperOutput.Content.GetContent().Should().NotContain(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEmptyValueInTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + sut.TextModel = new TextField(string.Empty); + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEditableFieldAndEditableSetToTrueAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.TextModel = testField; + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestEditableMarkup); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEmptyEditableFieldAndEditableSetToTrueAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = string.Empty + }; + sut.TextModel = testField; + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_ScTextTagWithEditableFieldAndEditableSetToFalseAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.Editable = false; + sut.TextModel = testField; + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + #endregion + + #region div tag with asp-for attribute tests + [Theory] + [AutoNSubstituteData] + public async Task Process_DivTagWithNullForAndTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = null!; + sut.TextModel = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithValidForAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = GetModelExpression(new RichTextField(TestHtml, false)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithValidForAttribute_GeneratesOutputWithOuterDivTag( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = GetModelExpression(new RichTextField(TestHtml, false)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be("div"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithInvalidForAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = GetModelExpression(new TextField(TestHtml)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEmptyValueInForAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = GetModelExpression(new TextField(string.Empty)); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestEditableMarkup); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEmptyEditableFieldAndEditableSetToTrue_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = string.Empty + }; + sut.For = GetModelExpression(testField); + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEditableFieldAndEditableSetToFalse_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + sut.TextModel = null; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + #endregion + + #region div tag with rich-text attribute tests + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithValidTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = null!; + sut.TextModel = new RichTextField(TestHtml, false); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithValidTextAttribute_GeneratesOutputWithOuterDivTag( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.TextModel = new RichTextField(TestHtml, false); + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.TagName.Should().Be("div"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithInvalidTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = null!; + sut.TextModel = new TextField(TestHtml); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEmptyValueInTextAttribute_GeneratesEmptyOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + sut.For = null!; + sut.TextModel = new TextField(string.Empty); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(string.Empty); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEditableFieldAndEditableSetToTrueAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = null!; + sut.TextModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestEditableMarkup); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEmptyEditableFieldAndEditableSetToTrueAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = string.Empty + }; + sut.TextModel = testField; + sut.For = null!; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + [Theory] + [AutoNSubstituteData] + public void Process_DivTagWithEditableFieldAndEditableSetToFalseAndTextAttribute_GeneratesCorrectOutput( + RichTextTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + tagHelperOutput.TagName = "div"; + RichTextField testField = new(TestHtml, false) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = null!; + sut.Editable = false; + sut.TextModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestHtml); + } + + #endregion + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs new file mode 100644 index 0000000..ab939d0 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs @@ -0,0 +1,203 @@ +using System.Globalization; +using System.Text.Encodings.Web; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers.Fields; + +public class TextFieldTagHelperFixture +{ + private const string TestEditableMarkup = "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}JSS Styleguide"; + private const string TestText = "This is the test text"; + private const string TestHtml = "

This is the test text

"; + private static readonly string TestMultilineText = $"

This is the test text {Environment.NewLine} with line endings.

"; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + TagHelperContext tagHelperContext = new( + [], + new Dictionary(), + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + + TagHelperOutput tagHelperOutput = new("span", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + return Task.FromResult(tagHelperContent); + }); + + f.Register(() => new TextFieldTagHelper()); + + f.Inject(tagHelperContext); + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_IsGuarded( + TextFieldTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + Action allNull = + () => sut.Process(null!, null!); + Action outputNull = + () => sut.Process(tagHelperContext, null!); + Action contextNull = + () => sut.Process(null!, tagHelperOutput); + + allNull.Should().Throw(); + outputNull.Should().Throw(); + contextNull.Should().Throw(); + } + + [Theory] + [AutoNSubstituteData] + public void Process_TextField_ReturnsEncodedHtml(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + sut.For = GetModelExpression(new TextField(TestHtml)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(HtmlEncoder.Default.Encode(TestHtml)); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(GetModelExpressionTestData), MemberType = typeof(TextFieldTagHelperFixture))] + public void Process_OnlyTextField_MustBeProcessed(ModelExpression model, string expectedOutput, TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + sut.For = model; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(expectedOutput); + } + + [Theory] + [AutoNSubstituteData] + public void Process_TextField_ShouldReplaceLineEndingsByDefault(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + sut.For = GetModelExpression(new TextField(TestMultilineText)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be("

This is the test text
with line endings.

"); + } + + [Theory] + [AutoNSubstituteData] + public void Process_TextField_ShouldNotReplaceLineEndingsIfConvertNewLinesIsFalse(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + sut.ConvertNewLines = false; + sut.For = GetModelExpression(new TextField(TestMultilineText)); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(HtmlEncoder.Default.Encode(TestMultilineText)); + } + + [Theory] + [AutoNSubstituteData] + public void Process_EditableTextFieldAndEditableSetToTrue_GeneratesCorrectOutput(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + TextField testField = new(TestText) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestEditableMarkup); + } + + [Theory] + [AutoNSubstituteData] + public void Process_EmptyEditableTextFieldAndEditableSetToTrue_GeneratesCorrectOutput(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + TextField testField = new(TestText) + { + EditableMarkup = string.Empty + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestText); + } + + [Theory] + [AutoNSubstituteData] + public void Process_EditableFieldAndEditableSetToFalse_GeneratesCorrectOutput(TextFieldTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + TextField testField = new(TestText) + { + EditableMarkup = TestEditableMarkup + }; + sut.For = GetModelExpression(testField); + sut.Editable = false; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestText); + } + + private static IEnumerable GetModelExpressionTestData() + { + yield return [null!, string.Empty]; + yield return [GetModelExpression(new TextField(TestHtml)), HtmlEncoder.Default.Encode(TestHtml)]; + yield return [GetModelExpression(new RichTextField(TestHtml)), string.Empty]; + yield return [GetModelExpression(new DateField(DateTime.UtcNow)), string.Empty]; + yield return [GetModelExpression(new CheckboxField(false)), string.Empty]; + } + + private static ModelExpression GetModelExpression(Field model) + { + DefaultModelMetadata? modelMetadata = Substitute.For( + Substitute.For(), + Substitute.For(), + Substitute.For(ModelMetadataIdentity.ForType(model.GetType()), ModelAttributes.GetAttributesForType(model.GetType()))); + ModelExplorer? explorer = Substitute.For(Substitute.For(), modelMetadata, model); + return new ModelExpression("test", explorer); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs new file mode 100644 index 0000000..ce58090 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs @@ -0,0 +1,585 @@ +using System.Globalization; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.TagHelpers; + +public class PlaceholderTagHelperFixture +{ + private const string TestComponentName = "TestComponent"; + + private const string PlaceHolderWithNoComponentsName = "empty-placeholder"; + + private const string PlaceHolderWithComponentsName = "populated-placeholder"; + + private static IComponentRendererFactory _componentRendererFactory = null!; + + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + // prevent nested recursion issues + f.Behaviors.Add(new OmitOnRecursionBehavior()); + + Component component = new() + { + Name = TestComponentName + }; + f.Inject(component); + + TestComponentRenderer componentRenderer = new(); + _componentRendererFactory = f.Freeze(); + _componentRendererFactory + .GetRenderer(Arg.Any()) + .Returns(componentRenderer); + + f.Inject(new EditableChromeRenderer()); + + // Configure the options - Required in ctor of PlaceholderTagHelper + IOptions? options = f.Freeze>(); + RenderingEngineOptions innerOptions = new() + { + RendererRegistry = new SortedList + { + { + 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer) + } + } + }; + options.Value.Returns(innerOptions); + + // Configure the View Context - required as property on PlaceholderTagHelper + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + + FeatureCollection features = new(); + + viewContext.HttpContext.Features.Returns(features); + f.Inject(viewContext); + + // Configure the TagHelperOutput - Required when Placeholder render logic is invoked + TagHelperOutput tagHelperOutput = new("placeholder", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + }); + + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameIsNotSet_OutputContainsHtmlComment( + PlaceholderTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + sut.Name = null; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(""); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameNotInLayoutServiceResponse_OutputContainsHtmlComment( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new Component + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = string.Empty, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = "unknown"; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(""); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_RenderingContextIsNull_ThrowsNullReferenceException( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + viewContext.HttpContext.Features[typeof(ISitecoreRenderingContext)] = null; + Func act = + () => sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("SitecoreLayout cannot be null."); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViewContextIsNull_ThrowsNullReferenceException( + PlaceholderTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + sut.ViewContext = null; + Func act = + () => sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Act & Assert + await act.Should().ThrowAsync().WithMessage("ViewContext parameter cannot be null."); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameInLayoutServiceResponseAndPlaceholderIsEmpty_OutputContainsHtmlComment( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithNoComponentsName] = [] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithNoComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($""); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameInLayoutServiceResponseAndPlaceholderIsNotEmpty_OutputContainsComponentHtml( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new Component + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = TestComponentName, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestComponentRenderer.HtmlContent); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderContainsComponentWithoutName_OutputContainsComponentHtml( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new Component + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = string.Empty, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be(TestComponentRenderer.HtmlContent); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderContainsComponentAndChrome_OutputContainsComponentAndChromeHtml( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new EditableChrome + { + Content = TestComponentRenderer.ChromeContent, + Attributes = new Dictionary + { + { + "type", "text/sitecore" + }, + { + "chrometype", "rendering" + }, + { + "kind", "open" + } + } + }, + + new Component + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = string.Empty, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + }, + + new EditableChrome + { + Attributes = new Dictionary + { + { + "type", "text/sitecore" + }, + { + "chrometype", "rendering" + }, + { + "kind", "close" + } + } + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be( + $"{TestComponentRenderer.ChromeContent}{TestComponentRenderer.HtmlContent}"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderContainsUnknownPlaceholderFeature_OutputIsEmpty( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new TestPlaceholderFeature + { + Content = TestComponentRenderer.HtmlContent + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().BeEmpty(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameInLayoutServiceResponseAndPlaceholderIsNotEmpty_ContextComponentDoNotChange( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + Component component = new(); + SitecoreRenderingContext context = new() + { + Component = component, + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = + [ + new Component + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = TestComponentName, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + } + ] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + context.Component.Should().Be(component); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_PlaceholderNameInLayoutServiceResponseAndPlaceholderIsNotEmpty_GetRendererWithRightComponent( + PlaceholderTagHelper sut, + ViewContext viewContext, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + Component component = new(); + Component placeholderFeature = new() + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + Name = TestComponentName, + Fields = + { + ["TestField1"] = new TextField("TestFieldValue1") + } + }; + + SitecoreRenderingContext context = new() + { + Component = component, + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + Placeholders = + { + [PlaceHolderWithComponentsName] = [placeholderFeature] + } + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.Name = PlaceHolderWithComponentsName; + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + _componentRendererFactory.Received().GetRenderer(placeholderFeature); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/ViewComponents/SitecoreComponentViewComponentFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/ViewComponents/SitecoreComponentViewComponentFixture.cs new file mode 100644 index 0000000..52e99e1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/ViewComponents/SitecoreComponentViewComponentFixture.cs @@ -0,0 +1,89 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; +using Sitecore.AspNetCore.SDK.RenderingEngine.Tests.Mocks; +using Sitecore.AspNetCore.SDK.RenderingEngine.ViewComponents; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Tests.ViewComponents; + +public class SitecoreComponentViewComponentFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup() + { + return f => + { + IViewModelBinder? viewModelBinder = Substitute.For(); + f.Inject(viewModelBinder); + f.Inject(new SitecoreComponentViewComponent(viewModelBinder)); + }; + } + + [Fact] + public void SitecoreComponentViewComponent_Ctor_Guarded() + { + Assert.Throws(() => new SitecoreComponentViewComponent(null!)); + } + + [Theory] + [AutoNSubstituteData] + public async Task InvokeAsync_ModelIsNull_ThrowsArgumentNullException(SitecoreComponentViewComponent sut) + { + // Arrange + + // Act + ArgumentNullException exception = await Assert.ThrowsAsync(() => sut.InvokeAsync(null!, null!)); + + // Assert + exception.Message.Should().Be("Value cannot be null. (Parameter 'modelType')"); + } + + [Theory] + [AutoNSubstituteData] + public async Task InvokeAsync_ViewNameIsNull_ThrowsArgumentNullException(SitecoreComponentViewComponent sut) + { + // Arrange + var model = new { PropertyA = "Test Property" }; + + // Act + ArgumentNullException exception = await Assert.ThrowsAsync(() => sut.InvokeAsync(model.GetType(), null!)); + + // Assert + exception.Message.Should().Be("Value cannot be null. (Parameter 'viewName')"); + } + + [Theory] + [AutoNSubstituteData] + public async Task InvokeAsync_ViewNameIsEmptyString_ThrowsArgumentNullException(SitecoreComponentViewComponent sut) + { + // Arrange + var model = new { PropertyA = "Test Property" }; + Func> act = + () => sut.InvokeAsync(model.GetType(), string.Empty); + + // Act & Assert + await act.Should().ThrowAsync().WithParameterName("viewName"); + } + + [Theory] + [AutoNSubstituteData] + public async Task InvokeAsync_ValidParameters(SitecoreComponentViewComponent sut, IViewModelBinder binder) + { + // Arrange + HeaderBlock model = new() { Heading1 = new TextField("Test Heading 1"), Heading2 = new TextField("Test Heading 2") }; + + // Act + _ = await sut.InvokeAsync(model.GetType(), "TestView"); + + // Assert + await binder.Received(1).Bind( + Arg.Is(typeof(HeaderBlock)), + Arg.Any()); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/TrackingApplicationConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/TrackingApplicationConfigurationExtensionsFixture.cs new file mode 100644 index 0000000..0caa8d5 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/TrackingApplicationConfigurationExtensionsFixture.cs @@ -0,0 +1,28 @@ +using AutoFixture; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Tracking.Tests.Extensions; + +public class TrackingApplicationConfigurationExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + IServiceProvider? services = Substitute.For(); + + f.Inject(services); + }; + + [Fact] + public void AddSitecoreTracking_NullServices_Throws() + { + // Arrange + Action action = () => TrackingAppConfigurationExtensions.WithTracking(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("serviceBuilder"); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/VisitorIdentificationConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/VisitorIdentificationConfigurationExtensionsFixture.cs new file mode 100644 index 0000000..34c8ae1 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Extensions/VisitorIdentificationConfigurationExtensionsFixture.cs @@ -0,0 +1,69 @@ +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Tracking.Tests.Extensions; + +public class VisitorIdentificationConfigurationExtensionsFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + IServiceProvider? services = Substitute.For(); + + f.Inject(services); + }; + + [Fact] + public void WithVisitorIdentification_NullServices_Throws() + { + // Arrange + Action action = () => VisitorIdentificationAppConfigurationExtensions.AddSitecoreVisitorIdentification(null!); + + // Act / Assert + action.Should().Throw() + .And.ParamName.Should().Be("services"); + } + + [Fact] + public void WithVisitorIdentification_NullOptions_NotThrows() + { + // Arrange + IServiceCollection serviceCollection = new ServiceCollection(); + Action action = () => serviceCollection.AddSitecoreVisitorIdentification(); + + // Act / Assert + action.Should().NotThrow(); + } + + [Fact] + public void UseSitecoreTracking_NullApp_Throws() + { + // Arrange + Action action = () => VisitorIdentificationAppConfigurationExtensions.UseSitecoreVisitorIdentification(null!); + + // Act / Assert + action.Should().Throw().And.ParamName.Should().Be("app"); + } + + [Theory] + [AutoNSubstituteData] + public void UseSitecoreTracking_NullChecksRenderingEngineOptionsUri_DosNotThrow(IApplicationBuilder app, IOptions trOptions) + { + // Arrange + app.ApplicationServices.GetService(typeof(IOptions)).Returns(trOptions); + + // Act + // Assert + Action(); + return; + + void Action() => app.UseSitecoreVisitorIdentification(); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Sitecore.AspNetCore.SDK.Tracking.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Sitecore.AspNetCore.SDK.Tracking.Tests.csproj new file mode 100644 index 0000000..efde05c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/Sitecore.AspNetCore.SDK.Tracking.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/TagHelpers/SitecoreVisitorIdentificationTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/TagHelpers/SitecoreVisitorIdentificationTagHelperFixture.cs new file mode 100644 index 0000000..c33f8dd --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Tracking.Tests/TagHelpers/SitecoreVisitorIdentificationTagHelperFixture.cs @@ -0,0 +1,279 @@ +using System.Collections; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.Providers; +using Sitecore.AspNetCore.SDK.Tracking.VisitorIdentification.TagHelpers; +using Xunit; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.Tracking.Tests.TagHelpers; + +public class SitecoreVisitorIdentificationTagHelperFixture +{ + // ReSharper disable once UnusedMember.Global - Used by testing framework + public static Action AutoSetup => f => + { + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + + FeatureCollection features = new(); + + viewContext.HttpContext.Features.Returns(features); + f.Inject(viewContext); + + TagHelperOutput tagHelperOutput = new("test", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + }); + + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestNoViewContext_OutputIsEmpty( + SitecoreVisitorIdentificationTagHelper sut, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().BeEmpty(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestOptionsNoUrl_OutputIsEmpty( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions() { SitecoreInstanceUri = null }); + + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().BeEmpty(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestNoGACookieResponseCookieFalse_OutputContainsJS( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + DateTime fakeDateTimeNowUtc) + { + // Arrange + IDateTimeProvider? fakeDateTimeProvider = Substitute.For(); + fakeDateTimeProvider.GetUtcNow().Returns(fakeDateTimeNowUtc); + + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions() { SitecoreInstanceUri = new Uri("https://testurl") }); + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + viewContext.HttpContext.RequestServices.GetService(typeof(IDateTimeProvider)).Returns(fakeDateTimeProvider); + + viewContext.HttpContext.Response.Headers.Returns(new HeaderDictionary(new Dictionary + { + { + "set-cookie", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($"\"/>"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestGACookieFalseResponseCookieFalse_OutputContainsJS( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + DateTime fakeDateTimeNowUtc) + { + // Arrange + IDateTimeProvider? fakeDateTimeProvider = Substitute.For(); + fakeDateTimeProvider.GetUtcNow().Returns(fakeDateTimeNowUtc); + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions { SitecoreInstanceUri = new Uri("https://testurl") }); + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + viewContext.HttpContext.RequestServices.GetService(typeof(IDateTimeProvider)).Returns(fakeDateTimeProvider); + + viewContext.HttpContext.Request.Cookies.Returns(new RequestCookies(new Dictionary + { + { + "SC_ANALYTICS_GLOBAL_COOKIE", "0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + viewContext.HttpContext.Response.Headers.Returns(new HeaderDictionary(new Dictionary + { + { + "set-cookie", "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($"\"/>"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestGACookieFalseResponseCookieTrue_OutputDoesNOTContainJS( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + DateTime fakeDateTimeNowUtc) + { + // Arrange + IDateTimeProvider? fakeDateTimeProvider = Substitute.For(); + fakeDateTimeProvider.GetUtcNow().Returns(fakeDateTimeNowUtc); + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions { SitecoreInstanceUri = new Uri("https://testurl") }); + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + viewContext.HttpContext.RequestServices.GetService(typeof(IDateTimeProvider)).Returns(fakeDateTimeProvider); + + viewContext.HttpContext.Request.Cookies.Returns(new RequestCookies(new Dictionary + { + { + "SC_ANALYTICS_GLOBAL_COOKIE", "0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + viewContext.HttpContext.Response.Headers.SetCookie.Returns(new StringValues( + "SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|True; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly")); + + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().NotBe($"\"/>"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestGACookieTrueResponseGACookieFalse_OutputContainsJS( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + DateTime fakeDateTimeNowUtc) + { + // Arrange + IDateTimeProvider? fakeDateTimeProvider = Substitute.For(); + fakeDateTimeProvider.GetUtcNow().Returns(fakeDateTimeNowUtc); + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions { SitecoreInstanceUri = new Uri("https://testurl") }); + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + viewContext.HttpContext.RequestServices.GetService(typeof(IDateTimeProvider)).Returns(fakeDateTimeProvider); + + viewContext.HttpContext.Request.Cookies.Returns(new RequestCookies(new Dictionary + { + { + "SC_ANALYTICS_GLOBAL_COOKIE", "0f82f53555ce4304a1ee8ae99ab9f9a8|True; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + viewContext.HttpContext.Response.Headers.SetCookie.Returns(new StringValues("SC_ANALYTICS_GLOBAL_COOKIE=0f82f53555ce4304a1ee8ae99ab9f9a8|False; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly")); + + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Be($"\"/>"); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_ViRequestGACookieTrueNoResponseGACookie_OutputDosNotContainJS( + SitecoreVisitorIdentificationTagHelper sut, + ViewContext viewContext, + IOptions stOptions, + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput, + DateTime fakeDateTimeNowUtc) + { + // Arrange + IDateTimeProvider? fakeDateTimeProvider = Substitute.For(); + fakeDateTimeProvider.GetUtcNow().Returns(fakeDateTimeNowUtc); + stOptions.Value.Returns(new SitecoreVisitorIdentificationOptions { SitecoreInstanceUri = new Uri("https://testurl") }); + viewContext.HttpContext.RequestServices.GetService(typeof(IOptions)).Returns(stOptions); + viewContext.HttpContext.RequestServices.GetService(typeof(IDateTimeProvider)).Returns(fakeDateTimeProvider); + + viewContext.HttpContext.Request.Cookies.Returns(new RequestCookies(new Dictionary + { + { + "SC_ANALYTICS_GLOBAL_COOKIE", "0f82f53555ce4304a1ee8ae99ab9f9a8|True; expires = Fri, 15 - Mar - 2030 13:15:08 GMT; path =/; HttpOnly" + } + })); + + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().NotBe($"\"/>"); + } + + private class RequestCookies(Dictionary cookies) + : Dictionary(cookies), IRequestCookieCollection + { + public new ICollection Keys => base.Keys; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/tests/Tests.GlobalSuppressions.cs b/tests/Tests.GlobalSuppressions.cs new file mode 100644 index 0000000..310dc49 --- /dev/null +++ b/tests/Tests.GlobalSuppressions.cs @@ -0,0 +1,19 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Not required.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Type confusion should not occur.")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "ReSharper ordering rules used.")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Underscores are used for private class variables.")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "Not required.")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "No headers required.")] + +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:DoNotUseRegions", Justification = "Large fixtures can use regions to group tests.")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Not required.")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1642:ConstructorSummaryDocumentationMustBeginWithStandardText", Justification = "Tests do not need to follow standard documentation rules.")] + +[assembly: SuppressMessage("Usage", "xUnit1045:Avoid using TheoryData type arguments that might not be serializable", Justification = "Individual Data rows aren't run.")] diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/CannedResponses.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/CannedResponses.cs new file mode 100644 index 0000000..a1ddc85 --- /dev/null +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/CannedResponses.cs @@ -0,0 +1,3922 @@ +using System.Globalization; +using System.Text.Encodings.Web; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Presentation; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using File = Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties.File; + +// ReSharper disable StringLiteralTypo +namespace Sitecore.AspNetCore.SDK.TestData; + +public static class CannedResponses +{ + public static SitecoreLayoutResponseContent Simple => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true, + Language = "en", + PageState = PageState.Normal, + Site = new Site + { + Name = "Test site" + } + }, + Route = new Route + { + DatabaseName = "db name", + DeviceId = "device id", + ItemId = "some id", + ItemLanguage = "some language", + ItemVersion = 50, + Name = "some name", + DisplayName = "display name", + LayoutId = "some layout id", + TemplateId = "some template id", + TemplateName = "some template name", + Fields = + { + ["checkbox"] = new CheckboxField(true), + ["date"] = new DateField(DateTime.Now), + ["number"] = new NumberField(25), + ["rich"] = new RichTextField("this is rich", false), + ["rich-encoded"] = new RichTextField("this is <b>rich</b>"), + ["text"] = new TextField("a value"), + ["hyperlink"] = new HyperLinkField(new HyperLink { Target = "/" }), + ["image"] = new ImageField(new Image { Src = "/images/thing.png" }), + ["item"] = new ItemLinkField { Id = Guid.NewGuid() } + }, + Placeholders = + { + ["p1"] = + [ + new Component + { + DataSource = "source", + Id = "an id", + Name = "a name", + Parameters = + { + ["alpha"] = "one" + }, + Fields = + { + ["checkbox"] = new CheckboxField(true), + ["date"] = new DateField(DateTime.Now), + ["number"] = new NumberField(25), + ["rich"] = new RichTextField("this is rich", false), + ["rich-encoded"] = new RichTextField("this is <b>rich</b>"), + ["text"] = new TextField("a value"), + ["hyperlink"] = new HyperLinkField(new HyperLink { Target = "/" }), + ["image"] = new ImageField(new Image { Src = "/images/thing.png" }), + ["item"] = new ItemLinkField { Id = Guid.NewGuid() } + } + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent SimpleWithContent => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true, + Language = "en", + PageState = PageState.Normal, + Site = new Site + { + Name = "Test site" + } + }, + Route = new Route + { + DatabaseName = "db name", + DeviceId = "device id", + ItemId = "some id", + ItemLanguage = "some language", + ItemVersion = 50, + Name = "some name", + LayoutId = "some layout id", + TemplateId = "some template id", + TemplateName = "some template name", + Fields = + { + ["content"] = new ContentListField { new() { Id = Guid.NewGuid() } }, + ["checkbox"] = new CheckboxField(true), + ["date"] = new DateField(DateTime.Now), + ["number"] = new NumberField(25), + ["rich"] = new RichTextField("this is rich", false), + ["rich-encoded"] = new RichTextField("this is <b>rich</b>"), + ["text"] = new TextField("a value"), + ["hyperlink"] = new HyperLinkField(new HyperLink { Target = "/" }), + ["image"] = new ImageField(new Image { Src = "/images/thing.png" }), + ["item"] = new ItemLinkField { Id = Guid.NewGuid() } + }, + Placeholders = + { + ["p1"] = + [ + new Component + { + DataSource = "source", + Id = "an id", + Name = "a name", + Parameters = + { + ["alpha"] = "one" + }, + Fields = + { + ["text1"] = new TextField("a value"), + ["hyperlink1"] = new HyperLinkField(new HyperLink { Target = "/" }), + ["image1"] = new ImageField(new Image { Src = "/images/thing.png" }), + ["item1"] = new ItemLinkField { Id = Guid.NewGuid() } + } + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent StyleGuide => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + DatabaseName = "available-in-connected-mode", + DeviceId = "available-in-connected-mode", + ItemId = "available-in-connected-mode", + ItemLanguage = "en", + ItemVersion = 1, + LayoutId = "available-in-connected-mode", + TemplateId = "available-in-connected-mode", + TemplateName = "available-in-connected-mode", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField("Styleguide | Sitecore JSS") + }, + Placeholders = + { + ["p1"] = + [ + new Component + { + DataSource = "source", + Id = "an id", + Name = "a name", + Parameters = + { + ["alpha"] = "one" + }, + Fields = + { + ["checkbox"] = new CheckboxField(true), + ["date"] = new DateField(DateTime.Now), + ["number"] = new NumberField(25), + ["rich"] = new RichTextField("this is rich", false), + ["rich-encoded"] = new RichTextField("this is <b>rich</b>"), + ["text"] = new TextField("a value"), + ["hyperlink"] = new HyperLinkField(new HyperLink { Target = "/" }), + ["image"] = new ImageField(new Image { Src = "/images/thing.png" }), + ["item"] = new ItemLinkField { Id = Guid.NewGuid() } + } + } + ], + ["jss-main"] = + [ + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CE}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Styleguide-Layout", + Placeholders = + { + ["jss-styleguide-layout"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Content Data") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{63B0C99E-DAC7-5670-9D66-C26A78000EAE}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Single-Line Text"), + ["sample"] = + new RichTextField( + "This is a sample text field. HTML is encoded. In Sitecore, editors will see a ."), + ["sample2"] = + new RichTextField( + "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.") + } + }, + + new Component + { + Id = "{F1EA3BB5-1175-5055-AB11-9C48BF69427A}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Multi-Line Text"), + ["description"] = + new RichTextField( + "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text."), + ["sample"] = + new RichTextField( + "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea."), + ["sample2"] = + new RichTextField( + "This is another sample multi-line text field using rendering options. HTML supported with encode=false.") + } + }, + + new Component + { + Id = "{69CEBC00-446B-5141-AD1E-450B8D6EE0AD}", + Name = "Styleguide-FieldUsage-RichText", + Fields = + { + ["heading"] = new TextField("Rich Text"), + ["sample"] = + new RichTextField( + "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

"), + ["sample2"] = new RichTextField( + "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n") + } + }, + + new Component + { + Id = "{5630C0E6-0430-5F6A-AF9E-2D09D600A386}", + Name = "Styleguide-FieldUsage-Image", + Fields = + { + ["heading"] = new TextField("Image"), + ["sample1"] = new ImageField(new Image + { + Src = "/data/media/img/sc_logo.png", + Alt = "Sitecore Logo" + }), + ["sample2"] = new ImageField(new Image + { + Src = "/data/media/img/jss_logo.png", + Alt = "Sitecore JSS Logo" + }) + } + }, + + new Component + { + Id = "{BAD43EF7-8940-504D-A09B-976C17A9A30C}", + Name = "Styleguide-FieldUsage-File", + Fields = + { + ["heading"] = new TextField("File"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["file"] = new FileField(new File + { + Src = "/data/media/files/jss.pdf", + Title = "Example File", + Description = + "This data will be added to the Sitecore Media Library on import" + }) + } + }, + + new Component + { + Id = "{FF90D4BD-E50D-5BBF-9213-D25968C9AE75}", + Name = "Styleguide-FieldUsage-Number", + Fields = + { + ["heading"] = new TextField("Number"), + ["description"] = + new RichTextField( + "Number tells Sitecore to use a number entry for editing."), + ["sample"] = new NumberField(1.21), + ["sample2"] = new NumberField(71), + } + }, + + new Component + { + Id = "{B5C1C74A-A81D-59B2-85D8-09BC109B1F70}", + Name = "Styleguide-FieldUsage-Checkbox", + Fields = + { + ["heading"] = new TextField("Checkbox"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["checkbox"] = new CheckboxField(true), + ["checkbox2"] = new CheckboxField(false), + } + }, + + new Component + { + Id = "{F166A7D6-9EC8-5C53-B825-33405DB7F575}", + Name = "Styleguide-FieldUsage-Date", + Fields = + { + ["heading"] = new TextField("Date"), + ["description"] = new RichTextField( + "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n"), + ["date"] = new DateField( + DateTime.Parse( + "2012-05-04T00:00:00Z", + CultureInfo.InvariantCulture)), + ["dateTime"] = + new DateField(DateTime.Parse( + "2018-03-14T15:00:00Z", + CultureInfo.InvariantCulture)), + } + }, + + new Component + { + Id = "{56A9562A-6813-579B-8ED2-FDDAB1BFD3D2}", + Name = "Styleguide-FieldUsage-Link", + Fields = + { + ["heading"] = new TextField("General Link"), + ["description"] = + new RichTextField( + "

A General Link is a field that represents an <a> tag.

"), + ["externalLink"] = new HyperLinkField(new HyperLink + { + Href = "https://www.sitecore.com", + Text = "Link to Sitecore" + }), + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/" }), + ["mediaLink"] = new HyperLinkField(new HyperLink + { + Href = "/data/media/files/jss.pdf", Text = "Link to PDF" + }), + ["emailLink"] = new HyperLinkField(new HyperLink + { + Href = "mailto:foo@bar.com", Text = "Send an Email" + }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = " title attribute" + }) + } + }, + + new Component + { + Id = "{A44AD1F8-0582-5248-9DF9-52429193A68B}", + Name = "Styleguide-FieldUsage-ItemLink", + Fields = + { + ["heading"] = new TextField("Item Link"), + ["description"] = new RichTextField( + "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField( + "ItemLink Demo (Shared) Item 1 Text Field") + } + }, + ["localItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField("Referenced item textField") + } + } + } + }, + + new Component + { + Id = "{2F609D40-8AD9-540E-901E-23AA2600F3EB}", + Name = "Styleguide-FieldUsage-ContentList", + Fields = + { + ["heading"] = new TextField("Content List"), + ["description"] = new RichTextField( + "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 1 Text Field") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 2 Text Field") + } + } + }, + ["localContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 1") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 2") + } + } + }, + } + }, + + new Component + { + Id = "{352ED63D-796A-5523-89F5-9A991DDA4A8F}", + Name = "Styleguide-FieldUsage-Custom", + Fields = + { + ["heading"] = new TextField("Custom Fields"), + ["description"] = new RichTextField( + "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n"), + ["customIntField"] = new Field { Value = 31337 } + } + } + ] + } + }, + + new Component + { + Id = "{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Layout Patterns") + }, + Placeholders = + { + ["jss-header"] = + [ + new Component + { + Id = "{1A6FCB1C-E97B-5325-8E4E-6E13997A4A1A}", + Name = "Styleguide-Layout-Reuse" + } + ], + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}", + Name = "Styleguide-Layout-Reuse", + Fields = + { + ["heading"] = new TextField("Reusing Content"), + ["description"] = new RichTextField( + "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

") + }, + Placeholders = + { + ["jss-reuse-example"] = + [ + new Component + { + Id = "{AA328B8A-D6E1-5B37-8143-250D2E93D6B8}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{C4330D34-623C-556C-BF4C-97C93D40FB1E}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{A42D8B1C-193D-5627-9130-F7F7F87617F1}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{0F4CB47A-979E-5139-B50B-A8E40C73C236}", + Name = "ContentBlock", + Fields = + { + ["content"] = + new RichTextField( + "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

") + } + } + ] + } + }, + + new Component + { + Id = "{538E4831-F157-50BB-AC74-277FCAC9FDDB}", + Name = "Styleguide-Layout-Tabs", + Fields = + { + ["heading"] = new TextField("Tabs"), + ["description"] = + new RichTextField( + "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

") + }, + Placeholders = + { + ["jss-tabs"] = + [ + new Component + { + Id = "{7ECB2ED2-AC9B-58D1-8365-10CA74824AF7}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 1"), + ["content"] = + new RichTextField("

Tab 1 contents!

") + } + }, + + new Component + { + Id = "{AFD64900-0A61-50EB-A674-A7A884E0D496}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 2"), + ["content"] = + new RichTextField("

Tab 2 contents!

") + } + }, + + new Component + { + Id = "{44C12983-3A84-5462-84C0-6CA1430050C8}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 3"), + ["content"] = + new RichTextField("

Tab 3 contents!

") + } + } + ] + } + } + ] + } + }, + + new Component + { + Id = "{2D806C25-DD46-51E3-93DE-63CF9035122C}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Sitecore Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{471FA16A-BB82-5C42-9C95-E7EAB1E3BD30}", + Name = "Styleguide-SitecoreContext", + Fields = + { + ["heading"] = new TextField("Sitecore Context"), + ["description"] = new RichTextField( + "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

") + } + }, + + new Component + { + Id = "{21F21053-8F8A-5436-BC79-E674E246A2FC}", + Name = "Styleguide-RouteFields", + Fields = + { + ["heading"] = new TextField("Route-level Fields"), + ["description"] = new RichTextField( + "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

") + } + }, + + new Component + { + Id = "{A0A66136-C21F-52E8-A2EA-F04DCFA6A027}", + Name = "Styleguide-ComponentParams", + Parameters = + { + ["cssClass"] = "alert alert-success", + ["columns"] = "5", + ["useCallToAction"] = "true" + }, + Fields = + { + ["heading"] = new TextField("Component Params"), + ["description"] = new RichTextField( + "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

") + } + }, + + new Component + { + Id = "{7F765FCB-3B10-58FD-8AA7-B346EF38C9BB}", + Name = "Styleguide-Tracking", + Fields = + { + ["heading"] = new TextField("Tracking"), + ["description"] = + new RichTextField( + "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

") + } + } + ] + } + }, + + new Component + { + Id = "{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Multilingual Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{CF1B5D2B-C949-56E7-9594-66AFACEACA9D}", + Name = "Styleguide-Multilingual", + Fields = + { + ["heading"] = new TextField("Translation Patterns"), + ["sample"] = + new TextField("This text can be translated in en.yml") + } + } + ] + } + } + ] + } + } + + ], + ["jss-styleguide-layout"] = [], + ["jss-styleguide-section"] = [], + ["jss-header"] = [], + ["jss-reuse-example"] = [], + ["jss-tabs"] = [] + } + } + } + }; + + public static SitecoreLayoutResponseContent StyleGuide1 => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + DatabaseName = "available-in-connected-mode", + DeviceId = "available-in-connected-mode", + ItemId = "available-in-connected-mode", + ItemLanguage = "en", + ItemVersion = 1, + LayoutId = SitecoreLayoutIds.Styleguide1LayoutId, + TemplateId = "available-in-connected-mode", + TemplateName = "available-in-connected-mode", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField("Styleguide | Sitecore JSS") + }, + Placeholders = + { + ["jss-main"] = + [ + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CE}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("ContentBlock 1 - JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CF}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("ContentBlock 2 - JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Styleguide-Layout", + Placeholders = + { + ["jss-styleguide-layout"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Content Data") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{63B0C99E-DAC7-5670-9D66-C26A78000EAE}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Single-Line Text"), + ["sample"] = + new RichTextField( + "This is a sample text field. HTML is encoded. In Sitecore, editors will see a ."), + ["sample2"] = + new RichTextField( + "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.") + } + }, + + new Component + { + Id = "{F1EA3BB5-1175-5055-AB11-9C48BF69427A}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = + new TextField( + $"This is {Environment.NewLine} Multi-Line Text"), + ["description"] = + new RichTextField( + "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text."), + ["sample"] = + new RichTextField( + "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea."), + ["sample2"] = + new RichTextField( + "This is another sample multi-line text field using rendering options. HTML supported with encode=false.") + } + }, + + new Component + { + Id = "{69CEBC00-446B-5141-AD1E-450B8D6EE0AD}", + Name = "Styleguide-FieldUsage-RichText", + Fields = + { + ["heading"] = new TextField("Rich Text"), + ["sample"] = + new RichTextField( + "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

"), + ["sample2"] = new RichTextField( + "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n") + } + }, + + new Component + { + Id = "{5630C0E6-0430-5F6A-AF9E-2D09D600A386}", + Name = "Styleguide-FieldUsage-Image", + Fields = + { + ["heading"] = new TextField("Image"), + ["sample1"] = new ImageField(new Image + { + Src = "/data/media/img/sc_logo.png", + Alt = "Sitecore Logo" + }), + ["sample2"] = new ImageField(new Image + { + Src = "/data/media/img/jss_logo.png", + Alt = "Sitecore JSS Logo" + }) + } + }, + + new Component + { + Id = "{BAD43EF7-8940-504D-A09B-976C17A9A30C}", + Name = "Styleguide-FieldUsage-File", + Fields = + { + ["heading"] = new TextField("File"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["file"] = new FileField(new File + { + Src = "/data/media/files/jss.pdf", + Title = "Example File", + Description = + "This data will be added to the Sitecore Media Library on import" + }) + } + }, + + new Component + { + Id = "{FF90D4BD-E50D-5BBF-9213-D25968C9AE75}", + Name = "Styleguide-FieldUsage-Number", + Fields = + { + ["heading"] = new TextField("Number"), + ["description"] = + new RichTextField( + "Number tells Sitecore to use a number entry for editing."), + ["sample"] = new NumberField(1.21), + ["sample2"] = new NumberField(71), + } + }, + + new Component + { + Id = "{B5C1C74A-A81D-59B2-85D8-09BC109B1F70}", + Name = "Styleguide-FieldUsage-Checkbox", + Fields = + { + ["heading"] = new TextField("Checkbox"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["checkbox"] = new CheckboxField(true), + ["checkbox2"] = new CheckboxField(false), + } + }, + + new Component + { + Id = "{F166A7D6-9EC8-5C53-B825-33405DB7F575}", + Name = "Styleguide-FieldUsage-Date", + Fields = + { + ["heading"] = new TextField("Date"), + ["description"] = new RichTextField( + "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n"), + ["date"] = new DateField( + DateTime.Parse( + "2012-05-04T00:00:00Z", + CultureInfo.InvariantCulture)), + ["dateTime"] = + new DateField(DateTime.Parse( + "2018-03-14T15:00:00Z", + CultureInfo.InvariantCulture)), + } + }, + + new Component + { + Id = "{56A9562A-6813-579B-8ED2-FDDAB1BFD3D2}", + Name = "Styleguide-FieldUsage-Link", + Fields = + { + ["heading"] = new TextField("General Link"), + ["description"] = + new RichTextField( + "

A General Link is a field that represents an <a> tag.

"), + ["externalLink"] = new HyperLinkField(new HyperLink + { + Href = "https://www.sitecore.com", + Text = "Link to Sitecore", Class = "link-class" + }), + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/" }), + ["mediaLink"] = new HyperLinkField(new HyperLink + { + Href = "/data/media/files/jss.pdf", Text = "Link to PDF" + }), + ["emailLink"] = new HyperLinkField(new HyperLink + { + Href = "mailto:foo@bar.com", Text = "Send an Email" + }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = " title attribute" + }) + } + }, + + new Component + { + Id = "{A44AD1F8-0582-5248-9DF9-52429193A68B}", + Name = "Styleguide-FieldUsage-ItemLink", + Fields = + { + ["heading"] = new TextField("Item Link"), + ["description"] = new RichTextField( + "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedItemLink"] = new ItemLinkField + { + Id = Guid.Parse("325c730f-91e6-53f8-adb4-9b35451a9e9e"), + Url = "/Content/Styleguide/ItemLinkField/Item1", + Fields = + { + ["textField"] = + new TextField( + "ItemLink Demo (Shared) Item 1 Text Field") + } + }, + ["localItemLink"] = new ItemLinkField + { + Id = Guid.Parse("f49a72d9-15e0-53d2-83f4-6b55ea815f62"), + Url = + "/styleguide/Page-Components/styleguide-jss-styleguide-section-B73482E131E5A083D77A50554BC74A4758E29636DF6824F6E2F272EE778C28A095/styleguide-jss-styleguide-section-B75151F05CFDC4CAFFE44E5BAED9D59BEA82565EC11CE75B7DEF3634495EC1DAB7", + Fields = + { + ["textField"] = + new TextField("Referenced item textField") + } + }, + } + }, + + new Component + { + Id = "{2F609D40-8AD9-540E-901E-23AA2600F3EB}", + Name = "Styleguide-FieldUsage-ContentList", + Fields = + { + ["heading"] = new TextField("Content List"), + ["description"] = new RichTextField( + "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedContentList"] = new ContentListField + { + new() + { + Id = Guid.Parse( + "5fbb46d4-5090-5c92-967a-89dcee9bb94f"), + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 1 Text Field") + } + }, + new() + { + Id = Guid.Parse( + "b5eea8da-9b23-51d4-b84a-e0d28af14f7c"), + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 2 Text Field") + } + } + }, + ["localContentList"] = new ContentListField + { + new() + { + Id = Guid.Parse( + "ed7fd1c3-35df-5d3d-8f9b-b75c97b1abe4"), + Fields = + { + ["textField"] = + new TextField("Hello World Item 1") + } + }, + new() + { + Id = Guid.Parse( + "94cd33e0-08da-525a-a73f-3adb5876424f"), + Fields = + { + ["textField"] = + new TextField("Hello World Item 2") + } + } + }, + } + }, + + new Component + { + Id = "{352ED63D-796A-5523-89F5-9A991DDA4A8F}", + Name = "Styleguide-FieldUsage-Custom", + Fields = + { + ["heading"] = new TextField("Custom Fields"), + ["description"] = new RichTextField( + "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n"), + ["customIntField"] = new Field { Value = 31337 } + } + } + ] + } + }, + + new Component + { + Id = "{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Layout Patterns") + }, + Placeholders = + { + ["jss-header"] = + [ + new Component + { + Id = "{1A6FCB1C-E97B-5325-8E4E-6E13997A4A1A}", + Name = "Styleguide-Layout-Reuse" + } + ], + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}", + Name = "Styleguide-Layout-Reuse", + Fields = + { + ["heading"] = new TextField("Reusing Content"), + ["description"] = new RichTextField( + "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

") + }, + Placeholders = + { + ["jss-reuse-example"] = + [ + new Component + { + Id = "{AA328B8A-D6E1-5B37-8143-250D2E93D6B8}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{C4330D34-623C-556C-BF4C-97C93D40FB1E}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{A42D8B1C-193D-5627-9130-F7F7F87617F1}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{0F4CB47A-979E-5139-B50B-A8E40C73C236}", + Name = "ContentBlock", + Fields = + { + ["content"] = + new RichTextField( + "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

") + } + } + ] + } + }, + + new Component + { + Id = "{538E4831-F157-50BB-AC74-277FCAC9FDDB}", + Name = "Styleguide-Layout-Tabs", + Fields = + { + ["heading"] = new TextField("Tabs"), + ["description"] = + new RichTextField( + "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

") + }, + Placeholders = + { + ["jss-tabs"] = + [ + new Component + { + Id = "{7ECB2ED2-AC9B-58D1-8365-10CA74824AF7}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 1"), + ["content"] = + new RichTextField("

Tab 1 contents!

") + } + }, + + new Component + { + Id = "{AFD64900-0A61-50EB-A674-A7A884E0D496}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 2"), + ["content"] = + new RichTextField("

Tab 2 contents!

") + } + }, + + new Component + { + Id = "{44C12983-3A84-5462-84C0-6CA1430050C8}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 3"), + ["content"] = + new RichTextField("

Tab 3 contents!

") + } + } + ] + } + } + ] + } + }, + + new Component + { + Id = "{2D806C25-DD46-51E3-93DE-63CF9035122C}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Sitecore Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{471FA16A-BB82-5C42-9C95-E7EAB1E3BD30}", + Name = "Styleguide-SitecoreContext", + Fields = + { + ["heading"] = new TextField("Sitecore Context"), + ["description"] = new RichTextField( + "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

") + } + }, + + new Component + { + Id = "{21F21053-8F8A-5436-BC79-E674E246A2FC}", + Name = "Styleguide-RouteFields", + Fields = + { + ["heading"] = new TextField("Route-level Fields"), + ["description"] = new RichTextField( + "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

") + } + }, + + new Component + { + Id = "{A0A66136-C21F-52E8-A2EA-F04DCFA6A027}", + Name = "Styleguide-ComponentParams", + Parameters = + { + ["cssClass"] = "alert alert-success", + ["columns"] = "5", + ["useCallToAction"] = "true" + }, + Fields = + { + ["heading"] = new TextField("Component Params"), + ["description"] = new RichTextField( + "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

") + } + }, + + new Component + { + Id = "{7F765FCB-3B10-58FD-8AA7-B346EF38C9BB}", + Name = "Styleguide-Tracking", + Fields = + { + ["heading"] = new TextField("Tracking"), + ["description"] = + new RichTextField( + "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

") + } + } + ] + } + }, + + new Component + { + Id = "{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Multilingual Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{CF1B5D2B-C949-56E7-9594-66AFACEACA9D}", + Name = "Styleguide-Multilingual", + Fields = + { + ["heading"] = new TextField("Translation Patterns"), + ["sample"] = + new TextField("This text can be translated in en.yml") + } + } + ] + } + } + ] + } + } + ], + ["jss-styleguide-layout"] = [], + ["jss-styleguide-section"] = [], + ["jss-header"] = [], + ["jss-reuse-example"] = [], + ["jss-tabs"] = [] + } + }, + Devices = + [ + new Device + { + Id = "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "885b8314-7d8c-4cbb-8000-01421ea8f406", + InstanceId = "43222d12-08c9-453b-ae96-d406ebb95126", + PlaceholderKey = "main" + }, + + new Rendering + { + Id = "ce4adcfb-7990-4980-83fb-a00c1e3673db", + InstanceId = "cf044ad9-0332-407a-abde-587214a2c808", + PlaceholderKey = "/main/centercolumn" + }, + + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "b343725a-3a93-446e-a9c8-3a2cbd3db489", + PlaceholderKey = "/main/centercolumn/content" + } + ] + }, + + new Device + { + Id = "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + PlaceholderKey = "content" + } + ] + } + ] + } + }; + + public static SitecoreLayoutResponseContent StyleGuide2 => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + DatabaseName = "available-in-connected-mode", + DeviceId = "available-in-connected-mode", + ItemId = "available-in-connected-mode", + ItemLanguage = "en", + ItemVersion = 1, + LayoutId = SitecoreLayoutIds.Styleguide2LayoutId, + TemplateId = "available-in-connected-mode", + TemplateName = "available-in-connected-mode", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField("Styleguide | Sitecore JSS") + }, + Placeholders = + { + ["jss-main"] = + [ + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CE}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Styleguide-Layout", + Placeholders = + { + ["jss-styleguide-layout"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Content Data") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{63B0C99E-DAC7-5670-9D66-C26A78000EAE}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Single-Line Text"), + ["sample"] = + new RichTextField( + "This is a sample text field. HTML is encoded. In Sitecore, editors will see a ."), + ["sample2"] = + new RichTextField( + "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.") + } + }, + + new Component + { + Id = "{F1EA3BB5-1175-5055-AB11-9C48BF69427A}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Multi-Line Text"), + ["description"] = + new RichTextField( + "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text."), + ["sample"] = + new RichTextField( + "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea."), + ["sample2"] = + new RichTextField( + "This is another sample multi-line text field using rendering options. HTML supported with encode=false.") + } + }, + + new Component + { + Id = "{69CEBC00-446B-5141-AD1E-450B8D6EE0AD}", + Name = "Styleguide-FieldUsage-RichText", + Fields = + { + ["heading"] = new TextField("Rich Text"), + ["sample"] = + new RichTextField( + "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

"), + ["sample2"] = new RichTextField( + "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n") + } + }, + + new Component + { + Id = "{5630C0E6-0430-5F6A-AF9E-2D09D600A386}", + Name = "Styleguide-FieldUsage-Image", + Fields = + { + ["heading"] = new TextField("Image"), + ["sample1"] = new ImageField(new Image + { + Src = "/data/media/img/sc_logo.png", + Alt = "Sitecore Logo" + }), + ["sample2"] = new ImageField(new Image + { + Src = "/data/media/img/jss_logo.png", + Alt = "Sitecore JSS Logo" + }) + } + }, + + new Component + { + Id = "{BAD43EF7-8940-504D-A09B-976C17A9A30C}", + Name = "Styleguide-FieldUsage-File", + Fields = + { + ["heading"] = new TextField("File"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["file"] = new FileField(new File + { + Src = "/data/media/files/jss.pdf", + Title = "Example File", + Description = + "This data will be added to the Sitecore Media Library on import" + }) + } + }, + + new Component + { + Id = "{FF90D4BD-E50D-5BBF-9213-D25968C9AE75}", + Name = "Styleguide-FieldUsage-Number", + Fields = + { + ["heading"] = new TextField("Number"), + ["description"] = + new RichTextField( + "Number tells Sitecore to use a number entry for editing."), + ["sample"] = new NumberField(1.21), + ["sample2"] = new NumberField(71), + } + }, + + new Component + { + Id = "{B5C1C74A-A81D-59B2-85D8-09BC109B1F70}", + Name = "Styleguide-FieldUsage-Checkbox", + Fields = + { + ["heading"] = new TextField("Checkbox"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["checkbox"] = new CheckboxField(true), + ["checkbox2"] = new CheckboxField(false), + } + }, + + new Component + { + Id = "{F166A7D6-9EC8-5C53-B825-33405DB7F575}", + Name = "Styleguide-FieldUsage-Date", + Fields = + { + ["heading"] = new TextField("Date"), + ["description"] = new RichTextField( + "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n"), + ["date"] = new DateField( + DateTime.Parse( + "2012-05-04T00:00:00Z", + CultureInfo.InvariantCulture)), + ["dateTime"] = + new DateField(DateTime.Parse( + "2018-03-14T15:00:00Z", + CultureInfo.InvariantCulture)), + } + }, + + new Component + { + Id = "{56A9562A-6813-579B-8ED2-FDDAB1BFD3D2}", + Name = "Styleguide-FieldUsage-Link", + Fields = + { + ["heading"] = new TextField("General Link"), + ["description"] = + new RichTextField( + "

A General Link is a field that represents an <a> tag.

"), + ["externalLink"] = new HyperLinkField(new HyperLink + { + Href = "https://www.sitecore.com", + Text = "Link to Sitecore" + }), + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/" }), + ["mediaLink"] = new HyperLinkField(new HyperLink + { + Href = "/data/media/files/jss.pdf", Text = "Link to PDF" + }), + ["emailLink"] = new HyperLinkField(new HyperLink + { + Href = "mailto:foo@bar.com", Text = "Send an Email" + }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = " title attribute" + }) + } + }, + + new Component + { + Id = "{A44AD1F8-0582-5248-9DF9-52429193A68B}", + Name = "Styleguide-FieldUsage-ItemLink", + Fields = + { + ["heading"] = new TextField("Item Link"), + ["description"] = new RichTextField( + "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField( + "ItemLink Demo (Shared) Item 1 Text Field") + } + }, + ["localItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField("Referenced item textField") + } + }, + } + }, + + new Component + { + Id = "{2F609D40-8AD9-540E-901E-23AA2600F3EB}", + Name = "Styleguide-FieldUsage-ContentList", + Fields = + { + ["heading"] = new TextField("Content List"), + ["description"] = new RichTextField( + "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 1 Text Field") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 2 Text Field") + } + } + }, + ["localContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 1") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 2") + } + } + }, + } + }, + + new Component + { + Id = "{352ED63D-796A-5523-89F5-9A991DDA4A8F}", + Name = "Styleguide-FieldUsage-Custom", + Fields = + { + ["heading"] = new TextField("Custom Fields"), + ["description"] = new RichTextField( + "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n"), + ["customIntField"] = new Field { Value = 31337 } + } + } + ] + } + }, + + new Component + { + Id = "{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Layout Patterns") + }, + Placeholders = + { + ["jss-header"] = + [ + new Component + { + Id = "{1A6FCB1C-E97B-5325-8E4E-6E13997A4A1A}", + Name = "Styleguide-Layout-Reuse" + } + ], + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}", + Name = "Styleguide-Layout-Reuse", + Fields = + { + ["heading"] = new TextField("Reusing Content"), + ["description"] = new RichTextField( + "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

") + }, + Placeholders = + { + ["jss-reuse-example"] = + [ + new Component + { + Id = "{AA328B8A-D6E1-5B37-8143-250D2E93D6B8}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{C4330D34-623C-556C-BF4C-97C93D40FB1E}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{A42D8B1C-193D-5627-9130-F7F7F87617F1}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{0F4CB47A-979E-5139-B50B-A8E40C73C236}", + Name = "ContentBlock", + Fields = + { + ["content"] = + new RichTextField( + "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

") + } + } + ] + } + }, + + new Component + { + Id = "{538E4831-F157-50BB-AC74-277FCAC9FDDB}", + Name = "Styleguide-Layout-Tabs", + Fields = + { + ["heading"] = new TextField("Tabs"), + ["description"] = + new RichTextField( + "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

") + }, + Placeholders = + { + ["jss-tabs"] = + [ + new Component + { + Id = "{7ECB2ED2-AC9B-58D1-8365-10CA74824AF7}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 1"), + ["content"] = + new RichTextField("

Tab 1 contents!

") + } + }, + + new Component + { + Id = "{AFD64900-0A61-50EB-A674-A7A884E0D496}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 2"), + ["content"] = + new RichTextField("

Tab 2 contents!

") + } + }, + + new Component + { + Id = "{44C12983-3A84-5462-84C0-6CA1430050C8}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 3"), + ["content"] = + new RichTextField("

Tab 3 contents!

") + } + } + ] + } + } + ] + } + }, + + new Component + { + Id = "{2D806C25-DD46-51E3-93DE-63CF9035122C}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Sitecore Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{471FA16A-BB82-5C42-9C95-E7EAB1E3BD30}", + Name = "Styleguide-SitecoreContext", + Fields = + { + ["heading"] = new TextField("Sitecore Context"), + ["description"] = new RichTextField( + "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

") + } + }, + + new Component + { + Id = "{21F21053-8F8A-5436-BC79-E674E246A2FC}", + Name = "Styleguide-RouteFields", + Fields = + { + ["heading"] = new TextField("Route-level Fields"), + ["description"] = new RichTextField( + "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

") + } + }, + + new Component + { + Id = "{A0A66136-C21F-52E8-A2EA-F04DCFA6A027}", + Name = "Styleguide-ComponentParams", + Parameters = + { + ["cssClass"] = "alert alert-success", + ["columns"] = "5", + ["useCallToAction"] = "true" + }, + Fields = + { + ["heading"] = new TextField("Component Params"), + ["description"] = new RichTextField( + "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

") + } + }, + + new Component + { + Id = "{7F765FCB-3B10-58FD-8AA7-B346EF38C9BB}", + Name = "Styleguide-Tracking", + Fields = + { + ["heading"] = new TextField("Tracking"), + ["description"] = + new RichTextField( + "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

") + } + } + ] + } + }, + + new Component + { + Id = "{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Multilingual Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{CF1B5D2B-C949-56E7-9594-66AFACEACA9D}", + Name = "Styleguide-Multilingual", + Fields = + { + ["heading"] = new TextField("Translation Patterns"), + ["sample"] = + new TextField("This text can be translated in en.yml") + } + } + ] + } + } + ] + } + } + ], + ["jss-styleguide-layout"] = [], + ["jss-styleguide-section"] = [], + ["jss-header"] = [], + ["jss-reuse-example"] = [], + ["jss-tabs"] = [] + } + }, + Devices = + [ + new Device + { + Id = "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "885b8314-7d8c-4cbb-8000-01421ea8f406", + InstanceId = "43222d12-08c9-453b-ae96-d406ebb95126", + PlaceholderKey = "main" + }, + + new Rendering + { + Id = "ce4adcfb-7990-4980-83fb-a00c1e3673db", + InstanceId = "cf044ad9-0332-407a-abde-587214a2c808", + PlaceholderKey = "/main/centercolumn" + }, + + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "b343725a-3a93-446e-a9c8-3a2cbd3db489", + PlaceholderKey = "/main/centercolumn/content" + } + ] + }, + + new Device + { + Id = "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + PlaceholderKey = "content" + } + ] + } + ] + } + }; + + public static SitecoreLayoutResponseContent WithVisitorIdentificationLayoutPlaceholder => new() + { + Sitecore = new SitecoreData + { + Context = + new Context + { + Language = TestConstants.Language, + IsEditing = false, + PageState = PageState.Normal, + Site = new Site { Name = "TestSiteName" } + }, + Route = new Route + { + LayoutId = TestConstants.VisitorIdentificationPageLayoutId, + DatabaseName = TestConstants.DatabaseName, + DeviceId = "test-device-id", + ItemId = TestConstants.TestItemId, + ItemLanguage = "en", + ItemVersion = 1, + TemplateId = "test-template-id", + TemplateName = "test-template-name", + Name = "styleguide", + } + } + }; + + public static SitecoreLayoutResponseContent StyleGuideWithComponentParameters => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + DatabaseName = "available-in-connected-mode", + DeviceId = "available-in-connected-mode", + ItemId = "available-in-connected-mode", + ItemLanguage = "en", + ItemVersion = 1, + LayoutId = "{E02DDB9B-A062-5E50-924A-1940D7E053C2}", + TemplateId = "available-in-connected-mode", + TemplateName = "available-in-connected-mode", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField("Styleguide | Sitecore JSS") + }, + Placeholders = + { + ["jss-main"] = + [ + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CE}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + }, + Parameters = new Dictionary + { + { "CmpParam1", "Value1" }, + { "CmpParam2", "Value2" }, + { "CmpParam3", "Value3" } + } + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent StyleGuide1WithoutRoute => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = null + } + }; + + public static SitecoreLayoutResponseContent StyleGuide1WithContext => new() + { + ContextRawData = "{" + + "\"testClass1\": {" + + " \"testString\": \"stringExample\", " + + "\"testInt\": 123, " + + "\"testtime\": \"2020-12-08T13:09:44.1255842+02:00\" " + + " }," + + "\"testClass2\": {" + + "\"testString\": \"stringExample2\"," + + "\"testInt\": 1234 " + + "}, " + + "\"singleProperty\":\"SinglePropertyData\", " + + "\"pageEditing\": false," + + "\"site\": {" + + "\"name\": " + + "\"jss-react-sample\"" + + "}," + + "\"pageState\": \"normal\"," + + "\"language\": \"en\"}", + Sitecore = new SitecoreData + { + Context = new Context + { + Language = "en", + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + DatabaseName = "test-database-name", + DeviceId = "available-in-connected-mode", + ItemId = "available-in-connected-mode", + ItemLanguage = "en", + ItemVersion = 1, + LayoutId = "{E02DDB9B-A062-5E50-924A-1940D7E053C1}", + TemplateId = "available-in-connected-mode", + TemplateName = "available-in-connected-mode", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField("Styleguide | Sitecore JSS") + }, + Placeholders = + { + ["jss-main"] = + [ + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CD}", + Name = "HeaderBlock", + Fields = + { + ["heading1"] = new TextField("HeaderBlock - This is heading1"), + ["heading2"] = new TextField("HeaderBlock - This is heading2"), + } + }, + + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CE}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("ContentBlock 1 - JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{E02DDB9B-A062-5E50-924A-1940D7E053CF}", + Name = "ContentBlock", + Fields = + { + ["heading"] = new TextField("ContentBlock 2 - JSS Styleguide"), + ["content"] = new RichTextField( + "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n") + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Styleguide-Layout", + Placeholders = + { + ["jss-styleguide-layout"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Content Data") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{63B0C99E-DAC7-5670-9D66-C26A78000EAE}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Single-Line Text"), + ["sample"] = + new RichTextField( + "This is a sample text field. HTML is encoded. In Sitecore, editors will see a ."), + ["sample2"] = + new RichTextField( + "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.") + } + }, + + new Component + { + Id = "{F1EA3BB5-1175-5055-AB11-9C48BF69427A}", + Name = "Styleguide-FieldUsage-Text", + Fields = + { + ["heading"] = new TextField("Multi-Line Text"), + ["description"] = + new RichTextField( + "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text."), + ["sample"] = + new RichTextField( + "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea."), + ["sample2"] = + new RichTextField( + "This is another sample multi-line text field using rendering options. HTML supported with encode=false.") + } + }, + + new Component + { + Id = "{69CEBC00-446B-5141-AD1E-450B8D6EE0AD}", + Name = "Styleguide-FieldUsage-RichText", + Fields = + { + ["heading"] = new TextField("Rich Text"), + ["sample"] = + new RichTextField( + "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

"), + ["sample2"] = new RichTextField( + "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n") + } + }, + + new Component + { + Id = "{5630C0E6-0430-5F6A-AF9E-2D09D600A386}", + Name = "Styleguide-FieldUsage-Image", + Fields = + { + ["heading"] = new TextField("Image"), + ["sample1"] = new ImageField(new Image + { + Src = "/data/media/img/sc_logo.png", + Alt = "Sitecore Logo" + }), + ["sample2"] = new ImageField(new Image + { + Src = "/data/media/img/jss_logo.png", + Alt = "Sitecore JSS Logo" + }) + } + }, + + new Component + { + Id = "{BAD43EF7-8940-504D-A09B-976C17A9A30C}", + Name = "Styleguide-FieldUsage-File", + Fields = + { + ["heading"] = new TextField("File"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["file"] = new FileField(new File + { + Src = "/data/media/files/jss.pdf", + Title = "Example File", + Description = + "This data will be added to the Sitecore Media Library on import" + }) + } + }, + + new Component + { + Id = "{FF90D4BD-E50D-5BBF-9213-D25968C9AE75}", + Name = "Styleguide-FieldUsage-Number", + Fields = + { + ["heading"] = new TextField("Number"), + ["description"] = + new RichTextField( + "Number tells Sitecore to use a number entry for editing."), + ["sample"] = new NumberField(1.21), + ["sample2"] = new NumberField(71), + } + }, + + new Component + { + Id = "{B5C1C74A-A81D-59B2-85D8-09BC109B1F70}", + Name = "Styleguide-FieldUsage-Checkbox", + Fields = + { + ["heading"] = new TextField("Checkbox"), + ["description"] = new RichTextField( + "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"), + ["checkbox"] = new CheckboxField(true), + ["checkbox2"] = new CheckboxField(false), + } + }, + + new Component + { + Id = "{F166A7D6-9EC8-5C53-B825-33405DB7F575}", + Name = "Styleguide-FieldUsage-Date", + Fields = + { + ["heading"] = new TextField("Date"), + ["description"] = new RichTextField( + "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n"), + ["date"] = new DateField( + DateTime.Parse( + "2012-05-04T00:00:00Z", + CultureInfo.InvariantCulture)), + ["dateTime"] = + new DateField(DateTime.Parse( + "2018-03-14T15:00:00Z", + CultureInfo.InvariantCulture)), + } + }, + + new Component + { + Id = "{56A9562A-6813-579B-8ED2-FDDAB1BFD3D2}", + Name = "Styleguide-FieldUsage-Link", + Fields = + { + ["heading"] = new TextField("General Link"), + ["description"] = + new RichTextField( + "

A General Link is a field that represents an <a> tag.

"), + ["externalLink"] = new HyperLinkField(new HyperLink + { + Href = "https://www.sitecore.com", + Text = "Link to Sitecore" + }), + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/" }), + ["mediaLink"] = new HyperLinkField(new HyperLink + { + Href = "/data/media/files/jss.pdf", + Text = "Link to PDF" + }), + ["emailLink"] = new HyperLinkField(new HyperLink + { + Href = "mailto:foo@bar.com", Text = "Send an Email" + }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = " title attribute" + }) + } + }, + + new Component + { + Id = "{A44AD1F8-0582-5248-9DF9-52429193A68B}", + Name = "Styleguide-FieldUsage-ItemLink", + Fields = + { + ["heading"] = new TextField("Item Link"), + ["description"] = new RichTextField( + "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField( + "ItemLink Demo (Shared) Item 1 Text Field") + } + }, + ["localItemLink"] = new ItemLinkField + { + Fields = + { + ["textField"] = + new TextField("Referenced item textField") + } + }, + } + }, + + new Component + { + Id = "{2F609D40-8AD9-540E-901E-23AA2600F3EB}", + Name = "Styleguide-FieldUsage-ContentList", + Fields = + { + ["heading"] = new TextField("Content List"), + ["description"] = new RichTextField( + "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"), + ["sharedContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 1 Text Field") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField( + "ContentList Demo (Shared) Item 2 Text Field") + } + } + }, + ["localContentList"] = new ContentListField + { + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 1") + } + }, + new() + { + Fields = + { + ["textField"] = + new TextField("Hello World Item 2") + } + } + }, + } + }, + + new Component + { + Id = "{352ED63D-796A-5523-89F5-9A991DDA4A8F}", + Name = "Styleguide-FieldUsage-Custom", + Fields = + { + ["heading"] = new TextField("Custom Fields"), + ["description"] = new RichTextField( + "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n"), + ["customIntField"] = new Field { Value = 31337 } + } + } + ] + } + }, + + new Component + { + Id = "{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Layout Patterns") + }, + Placeholders = + { + ["jss-header"] = + [ + new Component + { + Id = "{1A6FCB1C-E97B-5325-8E4E-6E13997A4A1A}", + Name = "Styleguide-Layout-Reuse" + } + ], + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}", + Name = "Styleguide-Layout-Reuse", + Fields = + { + ["heading"] = new TextField("Reusing Content"), + ["description"] = new RichTextField( + "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

") + }, + Placeholders = + { + ["jss-reuse-example"] = + [ + new Component + { + Id = "{AA328B8A-D6E1-5B37-8143-250D2E93D6B8}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{C4330D34-623C-556C-BF4C-97C93D40FB1E}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{A42D8B1C-193D-5627-9130-F7F7F87617F1}", + Name = "ContentBlock", + Fields = + { + ["content"] = new RichTextField( + "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

") + } + }, + + new Component + { + Id = "{0F4CB47A-979E-5139-B50B-A8E40C73C236}", + Name = "ContentBlock", + Fields = + { + ["content"] = + new RichTextField( + "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

") + } + } + ] + } + }, + + new Component + { + Id = "{538E4831-F157-50BB-AC74-277FCAC9FDDB}", + Name = "Styleguide-Layout-Tabs", + Fields = + { + ["heading"] = new TextField("Tabs"), + ["description"] = + new RichTextField( + "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

") + }, + Placeholders = + { + ["jss-tabs"] = + [ + new Component + { + Id = "{7ECB2ED2-AC9B-58D1-8365-10CA74824AF7}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 1"), + ["content"] = + new RichTextField( + "

Tab 1 contents!

") + } + }, + + new Component + { + Id = "{AFD64900-0A61-50EB-A674-A7A884E0D496}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 2"), + ["content"] = + new RichTextField( + "

Tab 2 contents!

") + } + }, + + new Component + { + Id = "{44C12983-3A84-5462-84C0-6CA1430050C8}", + Name = "Styleguide-Layout-Tabs-Tab", + Fields = + { + ["title"] = new TextField("Tab 3"), + ["content"] = + new RichTextField( + "

Tab 3 contents!

") + } + } + ] + } + } + ] + } + }, + + new Component + { + Id = "{2D806C25-DD46-51E3-93DE-63CF9035122C}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Sitecore Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{471FA16A-BB82-5C42-9C95-E7EAB1E3BD30}", + Name = "Styleguide-SitecoreContext", + Fields = + { + ["heading"] = new TextField("Sitecore Context"), + ["description"] = new RichTextField( + "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

") + } + }, + + new Component + { + Id = "{21F21053-8F8A-5436-BC79-E674E246A2FC}", + Name = "Styleguide-RouteFields", + Fields = + { + ["heading"] = new TextField("Route-level Fields"), + ["description"] = new RichTextField( + "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

") + } + }, + + new Component + { + Id = "{A0A66136-C21F-52E8-A2EA-F04DCFA6A027}", + Name = "Styleguide-ComponentParams", + Parameters = + { + ["cssClass"] = "alert alert-success", + ["columns"] = "5", + ["useCallToAction"] = "true" + }, + Fields = + { + ["heading"] = new TextField("Component Params"), + ["description"] = new RichTextField( + "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

") + } + }, + + new Component + { + Id = "{7F765FCB-3B10-58FD-8AA7-B346EF38C9BB}", + Name = "Styleguide-Tracking", + Fields = + { + ["heading"] = new TextField("Tracking"), + ["description"] = + new RichTextField( + "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

") + } + } + ] + } + }, + + new Component + { + Id = "{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}", + Name = "Styleguide-Section", + Fields = + { + ["heading"] = new TextField("Multilingual Patterns") + }, + Placeholders = + { + ["jss-styleguide-section"] = + [ + new Component + { + Id = "{CF1B5D2B-C949-56E7-9594-66AFACEACA9D}", + Name = "Styleguide-Multilingual", + Fields = + { + ["heading"] = new TextField("Translation Patterns"), + ["sample"] = + new TextField( + "This text can be translated in en.yml") + } + } + ] + } + } + ] + } + } + + ], + ["jss-styleguide-layout"] = [], + ["jss-styleguide-section"] = [], + ["jss-header"] = [], + ["jss-reuse-example"] = [], + ["jss-tabs"] = [] + } + } + } + }; + + public static SitecoreLayoutResponseContent WithNestedPlaceholder => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = TestConstants.Language, + IsEditing = false, + PageState = PageState.Normal, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + LayoutId = TestConstants.NestedPlaceholderPageLayoutId, + + DatabaseName = TestConstants.DatabaseName, + DeviceId = "test-device-id", + ItemId = TestConstants.TestItemId, + ItemLanguage = "en", + ItemVersion = 1, + TemplateId = "test-template-id", + TemplateName = "test-template-name", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField(TestConstants.PageTitle), + ["searchKeywords"] = new TextField(TestConstants.SearchKeywords) + }, + Placeholders = + { + ["placeholder-1"] = + [ + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Component-1", + Placeholders = + { + ["placeholder-2"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-2", + Fields = + { + ["TestField"] = new RichTextField(TestConstants.RichTextFieldValue1) + } + } + ] + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-3", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue), + ["EmptyField"] = new TextField(string.Empty), + ["NullValueField"] = new TextField(null!), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["EncodedField"] = new TextField(TestConstants.RichTextFieldValue1) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-4", + Fields = + { + ["Date"] = new DateField(DateTime.Parse("12.12.19", CultureInfo.InvariantCulture)), + ["RichTextField1"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["RichTextField2"] = new RichTextField(TestConstants.RichTextFieldValue2), + ["EmptyField"] = new RichTextField(string.Empty), + ["NullValueField"] = new RichTextField(null!), + ["TestField"] = new TextField(TestConstants.TestFieldValue) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-5", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue), + ["EmptyField"] = new TextField(string.Empty), + ["NullValueField"] = new TextField(null!), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["Date"] = new DateField(DateTime.Parse("12.12.19", CultureInfo.InvariantCulture)) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-6", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue + " from Component-6") + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Complex-Component", + Fields = + { + ["Header"] = new TextField(TestConstants.PageTitle), + ["Content"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["Header2"] = new TextField(TestConstants.TestFieldValue), + ["CustomField"] = new Models.CustomFieldType(TestConstants.TestFieldValue, "custom") + }, + Parameters = new Dictionary + { + { "ParamName", "ParamName-Value" } + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Custom-Model-Context-Component", + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-All-Field-Types", + Fields = + { + ["TextField"] = new TextField(TestConstants.TestFieldValue), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["RichTextField1"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["RichTextField2"] = new RichTextField(TestConstants.RichTextFieldValue2), + ["LinkField"] = new HyperLinkField(new HyperLink + { + Href = "/", Text = "Sample Link", Class = "sample", Target = "_blank", + Title = "title" + }), + ["ImageField"] = new ImageField(new Image { Alt = "sample", Src = "sample.png" }), + ["MediaLibraryItemImageField"] = new ImageField(new Image + { + Alt = "sample", + Src = + "https://cdinstance/en/-/media/094AED0302E7486880CB19926661FB77.ashx?h=51&w=204" + }), + ["DateField"] = new DateField(DateTime.Parse( + "2012-05-04T00:00:00Z", + CultureInfo.InvariantCulture)), + ["NumberField"] = new NumberField(9.99) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Links", + Fields = + { + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/", Text = "This is field text" }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = "title attribute" + }), + ["text"] = new TextField(TestConstants.TestFieldValue), + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Files", + Fields = + { + ["fileLink"] = new FileField(new File + { + MimeType = "application/pdf", + Description = "Download link description", + Src = "/doc.pdf", + Title = "Download link text" + }) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Images", + Fields = + { + ["FirstImage"] = new ImageField(new Image { Alt = "sample", Src = "sample.png" }), + ["SecondImage"] = new ImageField(new Image + { Alt = "second", Src = "site/second.png" }), + ["Heading"] = new TextField(TestConstants.TestFieldValue), + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Dates", + Fields = + { + ["date"] = new DateField(TestConstants.DateTimeValue), + ["text"] = new TextField(TestConstants.TestFieldValue) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Number", + Fields = + { + ["number"] = new NumberField(1.21), + ["text"] = new TextField(TestConstants.TestFieldValue) + } + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent WithMissingData => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + // missing context properties + }, + Route = new Route + { + LayoutId = TestConstants.NestedPlaceholderPageLayoutId, + + // missing route properties and fields + Placeholders = + { + ["placeholder-1"] = + [ + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Missing-Data", + + // missing component fields + Placeholders = + { + ["placeholder-2"] = + [ + new Component + { + // Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-Without-Id", + Fields = + { + ["TestField"] = new RichTextField(TestConstants.RichTextFieldValue1) + } + } + ] + } + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent WithMissingComponent => new() + { + Sitecore = new SitecoreData + { + Context = new Context(), + Route = new Route + { + LayoutId = TestConstants.MissingComponentPageLayoutId, + Placeholders = + { + ["placeholder-with-missing-component"] = + [ + new Component + { + Id = "{3C80CB13-D1DA-4946-8BC2-72ABF94D15E5}", + Name = "Component-3", + } + ] + } + } + } + }; + + public static SitecoreLayoutResponseContent EditablePage => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = TestConstants.Language, + IsEditing = true, + PageState = PageState.Edit, + Site = new Site + { + Name = "sample" + } + }, + Route = new Route + { + LayoutId = TestConstants.NestedPlaceholderPageLayoutId, + + DatabaseName = TestConstants.DatabaseName, + DeviceId = "test-device-id", + ItemId = TestConstants.TestItemId, + ItemLanguage = "en", + ItemVersion = 1, + TemplateId = "test-template-id", + TemplateName = "test-template-name", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField + { + Value = TestConstants.PageTitle, + EditableMarkup = "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}Styleguide | Sitecore JSS" + } + }, + Placeholders = + { + ["placeholder-1"] = + [ + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "open" }, + { "id", "placeholder_1" }, + { "key", "placeholder-1" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_E02DDB9BA0625E50924A1940D7E053CE" }, + { "hintname", "Component 1" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Component-1", + Placeholders = + { + ["placeholder-2"] = + [ + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "open" }, + { + "id", + "_placeholder_1_placeholder_2__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0" + }, + { + "key", + "/placeholder-1/placeholder-2-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0" + }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_B7C779DA2B75586CB40D081FCB864256" }, + { "hintname", "Component 2" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-2", + Fields = + { + ["TestField"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["ImageField"] = new ImageField(new Image + { + Alt = "Sitecore Logo", + Src = + "/sitecore/shell/-/jssmedia/styleguide/data/media/img/sc_logo.png?iar=0" + }) + { + EditableMarkup = + "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:rendering:editvariations({command:\\\"webedit:editvariations\\\"})\",\"header\":\"Edit variations\",\"icon\":\"/temp/iconcache/office/16x16/windows.png\",\"disabledIcon\":\"/temp/windows_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the variations.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{21218477-59D9-50C4-B7F4-FBCD69760250}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample1\",\"expandedDisplayName\":null}\"Sitecore" + } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 2" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "close" }, + { "id", "scEnclosingTag_" }, + { "hintname", "Placeholder-2" }, + { "class", "scpm" } + } + } + ] + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 1" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "close" }, + { "id", "scEnclosingTag_" }, + { "hintname", "Placeholder-1" }, + { "class", "scpm" } + } + } + ] + } + }, + Devices = + [ + new Device + { + Id = "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "885b8314-7d8c-4cbb-8000-01421ea8f406", + InstanceId = "43222d12-08c9-453b-ae96-d406ebb95126", + PlaceholderKey = "main" + }, + + new Rendering + { + Id = "ce4adcfb-7990-4980-83fb-a00c1e3673db", + InstanceId = "cf044ad9-0332-407a-abde-587214a2c808", + PlaceholderKey = "/main/centercolumn" + }, + + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "b343725a-3a93-446e-a9c8-3a2cbd3db489", + PlaceholderKey = "/main/centercolumn/content" + } + ] + }, + + new Device + { + Id = "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + PlaceholderKey = "content" + } + ] + } + ] + } + }; + + public static SitecoreLayoutResponseContent HorizonEditablePage => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = TestConstants.Language, + IsEditing = true, + PageState = PageState.Edit, + Site = new Site + { + Name = "sample" + } + }, + Route = new Route + { + LayoutId = TestConstants.NestedPlaceholderPageLayoutId, + + DatabaseName = TestConstants.DatabaseName, + DeviceId = "test-device-id", + ItemId = TestConstants.TestItemId, + ItemLanguage = "en", + ItemVersion = 1, + TemplateId = "test-template-id", + TemplateName = "test-template-name", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField + { + Value = TestConstants.PageTitle, + EditableMarkup = "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"fieldId\":\"152f40ed-fe76-5861-b425-522375549742\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}Styleguide | Sitecore JSS" + } + }, + Placeholders = + { + ["placeholder-1"] = + [ + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"jss-main\",\"placeholderMetadataKeys\":[\"jss-main\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"D8120D14950758B8BEF2E7A5158BD50F\",\"71AAF5C592425806BE7CECDF9154840B\",\"F351174D07C0547FBBDAEE51349C7DF5\",\"9B43C994B9835EC1A578401EA9478115\",\"FB33EEE82D6F5DD49CC71A5CBEBA728F\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "open" }, + { "id", "placeholder_1" }, + { "key", "placeholder-1" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{E02DDB9B-A062-5E50-924A-1940D7E053CE}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_E02DDB9BA0625E50924A1940D7E053CE" }, + { "hintname", "Component 1" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Component-1", + Placeholders = + { + ["placeholder-2"] = + [ + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout\",\"jss-styleguide-layout\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"A7BFC343F487521593488FBB0D209FA6\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-layout\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "open" }, + { + "id", + "_placeholder_1_placeholder_2__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0" + }, + { + "key", + "/placeholder-1/placeholder-2-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0" + }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{B7C779DA-2B75-586C-B40D-081FCB864256}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_B7C779DA2B75586CB40D081FCB864256" }, + { "hintname", "Component 2" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-2", + Fields = + { + ["TestField"] = new RichTextField(TestConstants.RichTextFieldValue1) + { + EditableMarkup = + "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}" + + TestConstants.RichTextFieldValue1 + "" + }, + ["EmptyField"] = new TextField(string.Empty) + { + EditableMarkup = + "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}" + } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 2" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "close" }, + { "id", "scEnclosingTag_" }, + { "hintname", "Placeholder-2" }, + { "class", "scpm" } + } + } + ] + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 1" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{B7C779DA-2B75-586C-B40D-081FCB864256}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_B7C779DA2B75586CB40D081FCB864256" }, + { "hintname", "Component 3" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-3", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue) + { + EditableMarkup = + "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"fieldId\":\"152f40ed-fe76-5861-b425-522375549742\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}" + + TestConstants.TestFieldValue + "" + } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 3" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = + "{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{B7C779DA-2B75-586C-B40D-081FCB864256}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "open" }, + { "id", "r_B7C779DA2B75586CB40D081FCB864256" }, + { "hintname", "Component 4" }, + { "class", "scpm" }, + { "data-selectable", "true" } + } + }, + + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-4", + Fields = + { + ["RichTextField1"] = new RichTextField(TestConstants.RichTextFieldValue1) + { + EditableMarkup = + "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}" + + TestConstants.RichTextFieldValue1 + "" + }, + ["RichTextField2"] = new RichTextField(TestConstants.RichTextFieldValue2) + { + EditableMarkup = + "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}" + + TestConstants.RichTextFieldValue2 + "" + } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "rendering" }, + { "kind", "close" }, + { "id", "scEnclosingTag_r_" }, + { "hintkey", "Component 4" }, + { "class", "scpm" } + } + }, + + new EditableChrome + { + Name = "code", + Type = "text/sitecore", + Content = string.Empty, + Attributes = new Dictionary + { + { "type", "text/sitecore" }, + { "chrometype", "placeholder" }, + { "kind", "close" }, + { "id", "scEnclosingTag_" }, + { "hintname", "Placeholder-1" }, + { "class", "scpm" } + } + } + ] + } + }, + Devices = + [ + new Device + { + Id = "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "885b8314-7d8c-4cbb-8000-01421ea8f406", + InstanceId = "43222d12-08c9-453b-ae96-d406ebb95126", + PlaceholderKey = "main" + }, + + new Rendering + { + Id = "ce4adcfb-7990-4980-83fb-a00c1e3673db", + InstanceId = "cf044ad9-0332-407a-abde-587214a2c808", + PlaceholderKey = "/main/centercolumn" + }, + + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "b343725a-3a93-446e-a9c8-3a2cbd3db489", + PlaceholderKey = "/main/centercolumn/content" + } + ] + }, + + new Device + { + Id = "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + LayoutId = "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + Renderings = + [ + new Rendering + { + Id = "493b3a83-0fa7-4484-8fc9-4680991cf743", + InstanceId = "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + PlaceholderKey = "content" + } + ] + } + ] + } + }; + + public static SitecoreLayoutResponseContent PageWithPreview => new() + { + Sitecore = new SitecoreData + { + Context = new Context + { + Language = TestConstants.Language, + IsEditing = false, + PageState = PageState.Preview, + Site = new Site + { + Name = "JssDisconnectedLayoutService" + } + }, + Route = new Route + { + LayoutId = TestConstants.NestedPlaceholderPageLayoutId, + + DatabaseName = TestConstants.DatabaseName, + DeviceId = "test-device-id", + ItemId = TestConstants.TestItemId, + ItemLanguage = "en", + ItemVersion = 1, + TemplateId = "test-template-id", + TemplateName = "test-template-name", + Name = "styleguide", + Fields = + { + ["pageTitle"] = new TextField(TestConstants.PageTitle), + ["searchKeywords"] = new TextField(TestConstants.SearchKeywords) + }, + Placeholders = + { + ["placeholder-1"] = + [ + new Component + { + Id = "{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}", + Name = "Component-1", + Placeholders = + { + ["placeholder-2"] = + [ + new Component + { + Id = "{B7C779DA-2B75-586C-B40D-081FCB864256}", + Name = "Component-2", + Fields = + { + ["TestField"] = new RichTextField(TestConstants.RichTextFieldValue1) + } + } + ] + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-3", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue), + ["EmptyField"] = new TextField(string.Empty), + ["NullValueField"] = new TextField(null!), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["EncodedField"] = new TextField(TestConstants.RichTextFieldValue1) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-4", + Fields = + { + ["Date"] = new DateField(DateTime.Parse("12.12.19", CultureInfo.InvariantCulture)), + ["RichTextField1"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["RichTextField2"] = new RichTextField(TestConstants.RichTextFieldValue2), + ["EmptyField"] = new RichTextField(string.Empty), + ["NullValueField"] = new RichTextField(null!), + ["TestField"] = new TextField(TestConstants.TestFieldValue) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-5", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue), + ["EmptyField"] = new TextField(string.Empty), + ["NullValueField"] = new TextField(null!), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["Date"] = new DateField(DateTime.Parse("12.12.19", CultureInfo.InvariantCulture)) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-6", + Fields = + { + ["TestField"] = new TextField(TestConstants.TestFieldValue + " from Component-6") + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Complex-Component", + Fields = + { + ["Header"] = new TextField(TestConstants.PageTitle), + ["Content"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["Header2"] = new TextField(TestConstants.TestFieldValue), + ["CustomField"] = new Models.CustomFieldType(TestConstants.TestFieldValue, "custom") + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-All-Field-Types", + Fields = + { + ["TextField"] = new TextField(TestConstants.TestFieldValue), + ["MultiLineField"] = new TextField(TestConstants.TestMultilineFieldValue), + ["RichTextField1"] = new RichTextField(TestConstants.RichTextFieldValue1), + ["RichTextField2"] = new RichTextField(TestConstants.RichTextFieldValue2), + ["LinkField"] = new HyperLinkField(new HyperLink + { + Href = "/", Text = "Sample Link", Class = "sample", Target = "_blank", + Title = "title" + }), + ["ImageField"] = new ImageField(new Image { Alt = "sample", Src = "sample.png" }), + ["NumberField"] = new NumberField(9.99) + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Links", + Fields = + { + ["internalLink"] = new HyperLinkField(new HyperLink + { Href = "/", Text = "This text should be ignored" }), + ["paramsLink"] = new HyperLinkField(new HyperLink + { + Href = "https://dev.sitecore.net", + Text = "Sitecore Dev Site", + Target = "_blank", + Class = "font-weight-bold", + Title = "title attribute" + }), + ["text"] = new TextField(TestConstants.TestFieldValue), + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Images", + Fields = + { + ["FirstImage"] = new ImageField(new Image { Alt = "sample", Src = "sample.png" }), + ["SecondImage"] = new ImageField(new Image + { Alt = "second", Src = "site/second.png" }), + ["Heading"] = new TextField(TestConstants.TestFieldValue), + } + }, + + new Component + { + Id = Guid.NewGuid().ToString(), + Name = "Component-With-Number", + Fields = + { + ["number"] = new NumberField(1.21), + ["text"] = new TextField(TestConstants.TestFieldValue) + } + } + ] + } + } + } + }; + + // ReSharper disable once MemberCanBePrivate.Global - Must be available + public static class SitecoreLayoutIds + { + public const string Styleguide1LayoutId = "{E02DDB9B-A062-5E50-924A-1940D7E053C1}"; + + public const string Styleguide2LayoutId = "{E02DDB9B-A062-5E50-924A-1940D7E053C2}"; + } +} \ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/Models/CustomFieldType.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/Models/CustomFieldType.cs new file mode 100644 index 0000000..4d6d193 --- /dev/null +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/Models/CustomFieldType.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.TestData.Models; + +public class CustomFieldType : Field +{ + [SetsRequiredMembers] + public CustomFieldType() + { + Value = string.Empty; + } + + [SetsRequiredMembers] + public CustomFieldType(string value1, string value2) + { + Value = value1 + value2; + } +} \ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/Serializer.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/Serializer.cs new file mode 100644 index 0000000..af3b880 --- /dev/null +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/Serializer.cs @@ -0,0 +1,23 @@ +using System.Text; +using System.Text.Json; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; + +namespace Sitecore.AspNetCore.SDK.TestData; + +public static class Serializer +{ + public static string Serialize(object obj) + { + return Encoding.UTF8.GetString(JsonSerializer.SerializeToUtf8Bytes(obj, GetOptions())); + } + + public static T? Deserialize(string data) + { + return JsonSerializer.Deserialize(data, GetOptions()); + } + + public static JsonSerializerOptions GetOptions() + { + return JsonLayoutServiceSerializer.GetDefaultSerializerOptions(); + } +} \ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj b/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj new file mode 100644 index 0000000..65e5a16 --- /dev/null +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs new file mode 100644 index 0000000..a76b12a --- /dev/null +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs @@ -0,0 +1,83 @@ +using System.Globalization; + +namespace Sitecore.AspNetCore.SDK.TestData; + +public static class TestConstants +{ + public const string TestFieldValue = "This is a test"; + + public const string RichTextFieldValue1 = "

This is a test

"; + public const string RichTextFieldValue2 = "

This is another test

"; + public const string LinkFieldValue = "
Sample Link"; + public const string DateFieldValue = "05/04/2012"; + public const string ImageFieldValue = "\"sample\""; + public const string AllFieldsImageValue = "\"sample\""; + public const string MediaLibraryItemImageFieldValue = "
Text
"; + public const string SecondImageValue = "\"second\""; + public const string SecondImageTestValue = "\"second\""; + + public const string NestedPlaceholderPageLayoutId = "NestedPlaceholderPageLayout"; + public const string MissingComponentPageLayoutId = "MissingComponentPageLayout"; + public const string VisitorIdentificationPageLayoutId = "VisitorIdentificationLayout"; + + public const string DatabaseName = "test-database-name"; + public const string TestItemId = "test-item-id"; + public const string PageTitle = "Styleguide | Sitecore JSS"; + public const string SearchKeywords = "styleguide,sitecore,jss"; + + public const string Language = "en"; + + public const string EEMiddlewarePostEndpoint = "/jss-render"; + + public const string SampleEndPoint = "/sample-Post-endpoint"; + + public const string SamplePostRouteEndPoint = "/sample-Post-Route-endpoint"; + + public const string EESampleRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/Sample\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EESampleRequestWithWrongRequestedSecret = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/Sample\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mywrongsecret"}"""; + + public const string EESampleRequestWithNoSecret = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/Sample\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle"}"""; + + public const string EELargeRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/Sample\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EEIncompleteRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io",\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle"}"""; + + public const string EESampleRoutingRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/UseRequestRouting\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EEDefaultRoutingRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/DefaultRoute\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EEEmptyRoutingRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"//\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EELongPathRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/Home/UseRequestRouting/Home/UseRequestRouting/test\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string EmptyPathRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"\":\"/\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string CaseSensitiveItemPathRequest = + """{"id":"jssdevex","args":["/?sc_httprenderengineurl=https%3a%2f%2f8eeabadd.ngrok.io","{\"sitecore\":{\"context\":{\"pageEditing\":false,\"site\":{\"name\":\"jssdevex\"},\"pageState\":\"normal\",\"language\":\"en\"},\"route\":{\"name\":\"home\",\"displayName\":\"home\",\"fields\":{\"pageTitle\":{\"value\":\"Welcome to Sitecore JSS\"}},\"databaseName\":\"master\",\"deviceId\":\"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\",\"itemId\":\"4e8410b0-28c5-52c5-8439-12a1ab247560\",\"itemLanguage\":\"en\",\"itemVersion\":1,\"layoutId\":\"80848506-1859-5f78-8fc6-f692c0c49795\",\"templateId\":\"6c0659f1-c66d-5877-a83b-510b6e0c64a2\",\"templateName\":\"App Route\",\"placeholders\":{\"jss-main\":[{\"uid\":\"2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d\",\"componentName\":\"ContentBlock\",\"dataSource\":\"{695CF95F-3E00-5B9F-A090-EB9C6D666DB5}\",\"params\":{},\"fields\":{\"heading\":{\"value\":\"Welcome to Sitecore JSS\"},\"content\":{\"value\":\"

Thanks for using JSS!! Here are some resources to get you started:

\\n\\n

Documentation

\\n

The official JSS documentation can help you with any JSS task from getting started to advanced techniques.

\\n\\n

Styleguide

\\n

The JSS styleguide is a living example of how to use JSS, hosted right in this app.\\nIt demonstrates most of the common patterns that JSS implementations may need to use,\\nas well as useful architectural patterns.

\\n\\n

GraphQL

\\n

JSS features integration with the Sitecore GraphQL API to enable fetching non-route data from Sitecore - or from other internal backends as an API aggregator or proxy.\\nThis route is a living example of how to use an integrate with GraphQL data in a JSS app.

\\n\\n
\\n

This app is a boilerplate

\\n

The JSS samples are a boilerplate, not a library. That means that any code in this app is meant for you to own and customize to your own requirements.

\\n

Want to change the lint settings? Do it. Want to read manifest data from a MongoDB database? Go for it. This app is yours.

\\n
\\n\\n
\\n

How to start with an empty app

\\n

To remove all of the default sample content (the Styleguide and GraphQL routes) and start out with an empty JSS app:

\\n
    \\n
  1. Delete /src/components/Styleguide* and /src/components/GraphQL*
  2. \\n
  3. Delete /sitecore/definitions/components/Styleguide*, /sitecore/definitions/templates/Styleguide*, and /sitecore/definitions/components/GraphQL*
  4. \\n
  5. Delete /data/component-content/Styleguide
  6. \\n
  7. Delete /data/content/Styleguide
  8. \\n
  9. Delete /data/routes/styleguide and /data/routes/graphql
  10. \\n
  11. Delete /data/dictionary/*.yml
  12. \\n
\\n
\\n\"}}}]}}}}","{\"language\":\"en\",\"dictionary\":{\"Documentation\":\"Documentation\",\"GraphQL\":\"GraphQL\",\"Styleguide\":\"Styleguide\",\"styleguide-sample\":\"This is a dictionary entry in English as a demonstration\"},\"httpContext\":{\"request\":{\"url\":\"https://jssdevex.dev.local:443/?sc_httprenderengineurl=https://8eeabadd.ngrok.io\",\"path\":\"/hoMe/userequestrouting\",\"querystring\":{\"sc_httprenderengineurl\":\"https://8eeabadd.ngrok.io\"},\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36 Edg/80.0.361.53\"}}}"],"functionName":"renderView","moduleName":"server.bundle","jssEditingSecret":"mysecret"}"""; + + public const string SampleResponseForEE = + """{"html":"{\u0022sitecore\u0022:{\u0022context\u0022:{\u0022pageEditing\u0022:false,\u0022site\u0022:{\u0022name\u0022:\u0022jssdevex\u0022},\u0022pageState\u0022:0,\u0022language\u0022:\u0022en\u0022},\u0022route\u0022:{\u0022databaseName\u0022:\u0022master\u0022,\u0022deviceId\u0022:\u0022fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3\u0022,\u0022itemId\u0022:\u00224e8410b0-28c5-52c5-8439-12a1ab247560\u0022,\u0022itemLanguage\u0022:\u0022en\u0022,\u0022itemVersion\u0022:1,\u0022layoutId\u0022:\u002280848506-1859-5f78-8fc6-f692c0c49795\u0022,\u0022templateId\u0022:\u00226c0659f1-c66d-5877-a83b-510b6e0c64a2\u0022,\u0022templateName\u0022:\u0022App Route\u0022,\u0022name\u0022:\u0022home\u0022,\u0022displayName\u0022:\u0022home\u0022,\u0022placeholders\u0022:{\u0022jss-main\u0022:[{}]},\u0022fields\u0022:{\u0022pageTitle\u0022:{}}},\u0022devices\u0022:[]},\u0022contextRawData\u0022:\u0022{\\u0022pageEditing\\u0022:false,\\u0022site\\u0022:{\\u0022name\\u0022:\\u0022jssdevex\\u0022},\\u0022pageState\\u0022:\\u0022normal\\u0022,\\u0022language\\u0022:\\u0022en\\u0022}\u0022}"}"""; + + public const string JssEditingSecret = "mysecret"; + + public const string TestParamNameValue = "ParamName-Value"; + +#pragma warning disable SA1401 +#pragma warning disable CA2211 + public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; + + public static DateTime DateTimeValue = DateTime.ParseExact("2012-05-04T00:00:00Z", "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); +#pragma warning restore CA2211 +#pragma warning restore SA1401 +} \ No newline at end of file diff --git a/tests/data/json/devices.json b/tests/data/json/devices.json new file mode 100644 index 0000000..db5a0f3 --- /dev/null +++ b/tests/data/json/devices.json @@ -0,0 +1,48 @@ +[ + { + "id": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "885b8314-7d8c-4cbb-8000-01421ea8f406", + "instanceId": "43222d12-08c9-453b-ae96-d406ebb95126", + "placeholderKey": "main", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "ce4adcfb-7990-4980-83fb-a00c1e3673db", + "instanceId": "cf044ad9-0332-407a-abde-587214a2c808", + "placeholderKey": "/main/centercolumn", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "b343725a-3a93-446e-a9c8-3a2cbd3db489", + "placeholderKey": "/main/centercolumn/content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + }, + { + "id": "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + "placeholderKey": "content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + } +] diff --git a/tests/data/json/edit-in-horizon-mode.json b/tests/data/json/edit-in-horizon-mode.json new file mode 100644 index 0000000..4873f65 --- /dev/null +++ b/tests/data/json/edit-in-horizon-mode.json @@ -0,0 +1,1870 @@ +{ + "sitecore": { + "context": { + "pageEditing": true, + "user": { + "domain": "sitecore", + "name": "Admin" + }, + "site": { + "name": "website" + }, + "pageState": "edit", + "language": "en" + }, + "route": { + "name": "styleguide", + "displayName": "styleguide", + "fields": { + "pageTitle": { + "value": "Styleguide | Sitecore JSS", + "editable": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"fieldId\":\"152f40ed-fe76-5861-b425-522375549742\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}Styleguide | Sitecore JSS" + } + }, + "databaseName": "master", + "deviceId": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "itemId": "8f7bef75-28a5-54f0-b7c4-998b51b67c75", + "itemLanguage": "en", + "itemVersion": 1, + "layoutId": "81c1f03a-fb2d-57e7-a346-0f0703701f62", + "templateId": "148edd4b-f2f1-586f-99e3-b69ad2ef6fe8", + "templateName": "App Route", + "placeholders": { + "jss-main": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"jss-main\",\"placeholderMetadataKeys\":[\"jss-main\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"D8120D14950758B8BEF2E7A5158BD50F\",\"71AAF5C592425806BE7CECDF9154840B\",\"F351174D07C0547FBBDAEE51349C7DF5\",\"9B43C994B9835EC1A578401EA9478115\",\"FB33EEE82D6F5DD49CC71A5CBEBA728F\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "jss_main", + "key": "jss-main", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{E02DDB9B-A062-5E50-924A-1940D7E053CE}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={A2484483-AF6F-5723-A29F-785E12CED97B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_E02DDB9BA0625E50924A1940D7E053CE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "e02ddb9b-a062-5e50-924a-1940d7e053ce", + "componentName": "ContentBlock", + "dataSource": "{A2484483-AF6F-5723-A29F-785E12CED97B}", + "params": {}, + "fields": { + "heading": { + "value": "JSS Styleguide", + "editable": "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"3f8e6ee3-8678-567b-a1fc-e5edb276cca3\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}JSS Styleguide" + }, + "content": { + "value": "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n", + "editable": "{\"contextItem\":{\"id\":\"a2484483-af6f-5723-a29f-785e12ced97b\",\"version\":1,\"language\":\"en\",\"revision\":\"c950fc1bd5484df88dc99bce389d51a0\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A2484483-AF6F-5723-A29F-785E12CED97B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"renderingId\":\"f351174d-07c0-547f-bbda-ee51349c7df5\",\"renderingInstanceId\":\"{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}\",\"editable\":true,\"commands\":[{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={F351174D-07C0-547F-BBDA-EE51349C7DF5},id={8F7BEF75-28A5-54F0-B7C4-998B51B67C75})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={F351174D-07C0-547F-BBDA-EE51349C7DF5},id={8F7BEF75-28A5-54F0-B7C4-998B51B67C75})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"F351174D07C0547FBBDAEE51349C7DF5\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout", + "id": "r_34A6553C81DE5CD3989E853F6CB6DF8C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "34a6553c-81de-5cd3-989e-853f6cb6df8c", + "componentName": "Styleguide-Layout", + "dataSource": "", + "params": {}, + "placeholders": { + "jss-styleguide-layout": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout\",\"jss-styleguide-layout\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"A7BFC343F487521593488FBB0D209FA6\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{B7C779DA-2B75-586C-B40D-081FCB864256}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={9F76F747-BC96-572A-BBCB-9D7655F98AC2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_B7C779DA2B75586CB40D081FCB864256", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "b7c779da-2b75-586c-b40d-081fcb864256", + "componentName": "Styleguide-Section", + "dataSource": "{9F76F747-BC96-572A-BBCB-9D7655F98AC2}", + "params": {}, + "fields": { + "heading": { + "value": "Content Data", + "editable": "{\"contextItem\":{\"id\":\"9f76f747-bc96-572a-bbcb-9d7655f98ac2\",\"version\":1,\"language\":\"en\",\"revision\":\"39157d6881cc4ef5aba1dae028fd4fb9\"},\"fieldId\":\"ac4286bb-8f0f-5ba1-b2d1-fca768658290\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9F76F747-BC96-572A-BBCB-9D7655F98AC2}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Content Data" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{B7C779DA-2B75-586C-B40D-081FCB864256}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section\",\"jss-styleguide-section\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"76FB99EF095B513DAA99CF0417582C61\",\"CE8CE858ED84594FAADC25691D673893\",\"5D5AA98C7F85542BBA008A760B25C350\",\"DF215418C334545FABA807C2DA2957DB\",\"308B7BEC89225FED848B22056304212B\",\"7FC8F8F3C129520CA9FC43B9F834F816\",\"DE5D5DE25D715629AB2E18A4BF4B1D6D\",\"D94F5D1FB1BB57C8A752DD6A3D283878\",\"84B833652AF85A79B719E5381BF8CDEF\",\"A9C5FAF6C0EF5E32A0E1A056393786AC\",\"D92203380E605AB6B894BABC4F9DC5B9\",\"B592151D5AD0513D8037440F550D9D4C\",\"1B482CC5C9015A46BC42444C31A3AC60\",\"4D93967180FB50D0AAF5FCF281CE5F4F\",\"A9E1268B25DA5D1EA5C771B410D32E4D\",\"C24FDF955E4150A9BD289279D40E259B\",\"18A84B80BE55538F827E4E55970138A1\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__B7C779DA_2B75_586C_B40D_081FCB864256__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{B7C779DA-2B75-586C-B40D-081FCB864256}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"df20174b-51a6-50f3-9a01-9d90d93a9861\",\"version\":1,\"language\":\"en\",\"revision\":\"c14436b8eaf246fe8ef9e4695d2cfd88\"},\"renderingId\":\"f7f8abe4-dfbd-526c-bec9-6cff8d8f8c2e\",\"renderingInstanceId\":\"{63B0C99E-DAC7-5670-9D66-C26A78000EAE}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={DF20174B-51A6-50F3-9A01-9D90D93A9861})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={63B0C99E-DAC7-5670-9D66-C26A78000EAE},renderingId={F7F8ABE4-DFBD-526C-BEC9-6CFF8D8F8C2E},id={DF20174B-51A6-50F3-9A01-9D90D93A9861})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={63B0C99E-DAC7-5670-9D66-C26A78000EAE},renderingId={F7F8ABE4-DFBD-526C-BEC9-6CFF8D8F8C2E},id={DF20174B-51A6-50F3-9A01-9D90D93A9861})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{DF20174B-51A6-50F3-9A01-9D90D93A9861}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Text\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Text", + "id": "r_63B0C99EDAC756709D66C26A78000EAE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "63b0c99e-dac7-5670-9d66-c26a78000eae", + "componentName": "Styleguide-FieldUsage-Text", + "dataSource": "{DF20174B-51A6-50F3-9A01-9D90D93A9861}", + "params": {}, + "fields": { + "sample": { + "value": "This is a sample text field. HTML is encoded. In Sitecore, editors will see a .", + "editable": "{\"contextItem\":{\"id\":\"df20174b-51a6-50f3-9a01-9d90d93a9861\",\"version\":1,\"language\":\"en\",\"revision\":\"c14436b8eaf246fe8ef9e4695d2cfd88\"},\"fieldId\":\"ed87cf03-b986-5ed6-b1d8-ec3482e9a3aa\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{DF20174B-51A6-50F3-9A01-9D90D93A9861}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}This is a sample text field. <mark>HTML is encoded.</mark> In Sitecore, editors will see a <input type="text">." + }, + "sample2": { + "value": "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.", + "editable": "{\"contextItem\":{\"id\":\"df20174b-51a6-50f3-9a01-9d90d93a9861\",\"version\":1,\"language\":\"en\",\"revision\":\"c14436b8eaf246fe8ef9e4695d2cfd88\"},\"fieldId\":\"5e3fef03-1aa8-5104-8b1e-6013d08d6e1d\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{DF20174B-51A6-50F3-9A01-9D90D93A9861}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}This is another sample text field using rendering options. <mark>HTML supported with encode=false.</mark> Cannot edit because editable=false." + }, + "heading": { + "value": "Single-Line Text", + "editable": "{\"contextItem\":{\"id\":\"df20174b-51a6-50f3-9a01-9d90d93a9861\",\"version\":1,\"language\":\"en\",\"revision\":\"c14436b8eaf246fe8ef9e4695d2cfd88\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{DF20174B-51A6-50F3-9A01-9D90D93A9861}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Single-Line Text" + }, + "description": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"df20174b-51a6-50f3-9a01-9d90d93a9861\",\"version\":1,\"language\":\"en\",\"revision\":\"c14436b8eaf246fe8ef9e4695d2cfd88\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{DF20174B-51A6-50F3-9A01-9D90D93A9861}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Text", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"35dcee0f-ceca-5045-8451-3de0641efb09\",\"version\":1,\"language\":\"en\",\"revision\":\"cc0e001c235a441a8508bc3aa21d97d5\"},\"renderingId\":\"f7f8abe4-dfbd-526c-bec9-6cff8d8f8c2e\",\"renderingInstanceId\":\"{F1EA3BB5-1175-5055-AB11-9C48BF69427A}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={35DCEE0F-CECA-5045-8451-3DE0641EFB09})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={F1EA3BB5-1175-5055-AB11-9C48BF69427A},renderingId={F7F8ABE4-DFBD-526C-BEC9-6CFF8D8F8C2E},id={35DCEE0F-CECA-5045-8451-3DE0641EFB09})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={F1EA3BB5-1175-5055-AB11-9C48BF69427A},renderingId={F7F8ABE4-DFBD-526C-BEC9-6CFF8D8F8C2E},id={35DCEE0F-CECA-5045-8451-3DE0641EFB09})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{35DCEE0F-CECA-5045-8451-3DE0641EFB09}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Text\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Text", + "id": "r_F1EA3BB511755055AB119C48BF69427A", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "f1ea3bb5-1175-5055-ab11-9c48bf69427a", + "componentName": "Styleguide-FieldUsage-Text", + "dataSource": "{35DCEE0F-CECA-5045-8451-3DE0641EFB09}", + "params": {}, + "fields": { + "sample": { + "value": "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea.", + "editable": "{\"contextItem\":{\"id\":\"35dcee0f-ceca-5045-8451-3de0641efb09\",\"version\":1,\"language\":\"en\",\"revision\":\"cc0e001c235a441a8508bc3aa21d97d5\"},\"fieldId\":\"ed87cf03-b986-5ed6-b1d8-ec3482e9a3aa\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{35DCEE0F-CECA-5045-8451-3DE0641EFB09}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}This is a sample multi-line text field. <mark>HTML is encoded.</mark> In Sitecore, editors will see a textarea." + }, + "sample2": { + "value": "This is another sample multi-line text field using rendering options. HTML supported with encode=false.", + "editable": "{\"contextItem\":{\"id\":\"35dcee0f-ceca-5045-8451-3de0641efb09\",\"version\":1,\"language\":\"en\",\"revision\":\"cc0e001c235a441a8508bc3aa21d97d5\"},\"fieldId\":\"5e3fef03-1aa8-5104-8b1e-6013d08d6e1d\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{35DCEE0F-CECA-5045-8451-3DE0641EFB09}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}This is another sample multi-line text field using rendering options. <mark>HTML supported with encode=false.</mark>" + }, + "heading": { + "value": "Multi-Line Text", + "editable": "{\"contextItem\":{\"id\":\"35dcee0f-ceca-5045-8451-3de0641efb09\",\"version\":1,\"language\":\"en\",\"revision\":\"cc0e001c235a441a8508bc3aa21d97d5\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{35DCEE0F-CECA-5045-8451-3DE0641EFB09}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Multi-Line Text" + }, + "description": { + "value": "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text.", + "editable": "{\"contextItem\":{\"id\":\"35dcee0f-ceca-5045-8451-3de0641efb09\",\"version\":1,\"language\":\"en\",\"revision\":\"cc0e001c235a441a8508bc3aa21d97d5\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{35DCEE0F-CECA-5045-8451-3DE0641EFB09}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text." + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Text", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"e0644fe1-1ecc-5cc1-893e-e4d098b2ec3d\",\"version\":1,\"language\":\"en\",\"revision\":\"a3c14ef99b8e42d1871360132de7435f\"},\"renderingId\":\"76fb99ef-095b-513d-aa99-cf0417582c61\",\"renderingInstanceId\":\"{69CEBC00-446B-5141-AD1E-450B8D6EE0AD}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={69CEBC00-446B-5141-AD1E-450B8D6EE0AD},renderingId={76FB99EF-095B-513D-AA99-CF0417582C61},id={E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={69CEBC00-446B-5141-AD1E-450B8D6EE0AD},renderingId={76FB99EF-095B-513D-AA99-CF0417582C61},id={E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"76FB99EF095B513DAA99CF0417582C61\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-RichText\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-RichText", + "id": "r_69CEBC00446B5141AD1E450B8D6EE0AD", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "69cebc00-446b-5141-ad1e-450b8d6ee0ad", + "componentName": "Styleguide-FieldUsage-RichText", + "dataSource": "{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}", + "params": {}, + "fields": { + "sample": { + "value": "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

", + "editable": "{\"contextItem\":{\"id\":\"e0644fe1-1ecc-5cc1-893e-e4d098b2ec3d\",\"version\":1,\"language\":\"en\",\"revision\":\"a3c14ef99b8e42d1871360132de7435f\"},\"fieldId\":\"a09fe08e-d070-57a0-b8a7-8fbbde83ec5c\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

" + }, + "sample2": { + "value": "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n", + "editable": "{\"contextItem\":{\"id\":\"e0644fe1-1ecc-5cc1-893e-e4d098b2ec3d\",\"version\":1,\"language\":\"en\",\"revision\":\"a3c14ef99b8e42d1871360132de7435f\"},\"fieldId\":\"ccb6a3b7-c505-50f5-8a96-a134533de530\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n
" + }, + "heading": { + "value": "Rich Text", + "editable": "{\"contextItem\":{\"id\":\"e0644fe1-1ecc-5cc1-893e-e4d098b2ec3d\",\"version\":1,\"language\":\"en\",\"revision\":\"a3c14ef99b8e42d1871360132de7435f\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Rich Text" + }, + "description": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"e0644fe1-1ecc-5cc1-893e-e4d098b2ec3d\",\"version\":1,\"language\":\"en\",\"revision\":\"a3c14ef99b8e42d1871360132de7435f\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E0644FE1-1ECC-5CC1-893E-E4D098B2EC3D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-RichText", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"f22cca9b-7c76-58be-8f1d-e20aca22b131\",\"version\":1,\"language\":\"en\",\"revision\":\"d02809093b9645318cf619ec1dcad0d4\"},\"renderingId\":\"ce8ce858-ed84-594f-aadc-25691d673893\",\"renderingInstanceId\":\"{5630C0E6-0430-5F6A-AF9E-2D09D600A386}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample1|sample2|heading|description,id={F22CCA9B-7C76-58BE-8F1D-E20ACA22B131})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={5630C0E6-0430-5F6A-AF9E-2D09D600A386},renderingId={CE8CE858-ED84-594F-AADC-25691D673893},id={F22CCA9B-7C76-58BE-8F1D-E20ACA22B131})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={5630C0E6-0430-5F6A-AF9E-2D09D600A386},renderingId={CE8CE858-ED84-594F-AADC-25691D673893},id={F22CCA9B-7C76-58BE-8F1D-E20ACA22B131})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"CE8CE858ED84594FAADC25691D673893\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Image\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Image", + "id": "r_5630C0E604305F6AAF9E2D09D600A386", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "5630c0e6-0430-5f6a-af9e-2d09d600a386", + "componentName": "Styleguide-FieldUsage-Image", + "dataSource": "{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}", + "params": {}, + "fields": { + "sample1": { + "value": { + "src": "/sitecore/shell/-/media/test-app-one/data/media/img/sc_logo.ashx?iar=0", + "alt": "Sitecore Logo" + }, + "editable": "{\"contextItem\":{\"id\":\"f22cca9b-7c76-58be-8f1d-e20aca22b131\",\"version\":1,\"language\":\"en\",\"revision\":\"d02809093b9645318cf619ec1dcad0d4\"},\"fieldId\":\"11c14456-301b-5770-b6a5-76299e87c292\",\"fieldType\":\"Image\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample1\",\"expandedDisplayName\":null}\"Sitecore" + }, + "sample2": { + "value": { + "src": "/sitecore/shell/-/media/test-app-one/data/media/img/jss_logo.ashx?iar=0", + "alt": "Sitecore JSS Logo" + }, + "editable": "{\"contextItem\":{\"id\":\"f22cca9b-7c76-58be-8f1d-e20aca22b131\",\"version\":1,\"language\":\"en\",\"revision\":\"d02809093b9645318cf619ec1dcad0d4\"},\"fieldId\":\"ed99039d-ea11-5065-af0a-20c3a4d2c64f\",\"fieldType\":\"Image\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample2\",\"expandedDisplayName\":null}\"Sitecore" + }, + "heading": { + "value": "Image", + "editable": "{\"contextItem\":{\"id\":\"f22cca9b-7c76-58be-8f1d-e20aca22b131\",\"version\":1,\"language\":\"en\",\"revision\":\"d02809093b9645318cf619ec1dcad0d4\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Image" + }, + "description": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"f22cca9b-7c76-58be-8f1d-e20aca22b131\",\"version\":1,\"language\":\"en\",\"revision\":\"d02809093b9645318cf619ec1dcad0d4\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F22CCA9B-7C76-58BE-8F1D-E20ACA22B131}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Image", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"4bd77c0e-44b2-5d46-96fc-9a4120859b5d\",\"version\":1,\"language\":\"en\",\"revision\":\"9ffdc0d9a585441d90bc780ef1c2ca3b\"},\"renderingId\":\"5d5aa98c-7f85-542b-ba00-8a760b25c350\",\"renderingInstanceId\":\"{BAD43EF7-8940-504D-A09B-976C17A9A30C}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=file|heading|description,id={4BD77C0E-44B2-5D46-96FC-9A4120859B5D})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={BAD43EF7-8940-504D-A09B-976C17A9A30C},renderingId={5D5AA98C-7F85-542B-BA00-8A760B25C350},id={4BD77C0E-44B2-5D46-96FC-9A4120859B5D})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={BAD43EF7-8940-504D-A09B-976C17A9A30C},renderingId={5D5AA98C-7F85-542B-BA00-8A760B25C350},id={4BD77C0E-44B2-5D46-96FC-9A4120859B5D})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4BD77C0E-44B2-5D46-96FC-9A4120859B5D}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"5D5AA98C7F85542BBA008A760B25C350\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-File\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-File", + "id": "r_BAD43EF78940504DA09B976C17A9A30C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "bad43ef7-8940-504d-a09b-976c17a9a30c", + "componentName": "Styleguide-FieldUsage-File", + "dataSource": "{4BD77C0E-44B2-5D46-96FC-9A4120859B5D}", + "params": {}, + "fields": { + "file": { + "value": { + "src": "/sitecore/shell/-/media/test-app-one/data/media/files/jss.ashx", + "name": "jss", + "displayName": "jss", + "title": "Example File", + "keywords": "", + "description": "This data will be added to the Sitecore Media Library on import", + "extension": "pdf", + "mimeType": "application/pdf", + "size": "156897" + }, + "editable": "{\"contextItem\":{\"id\":\"4bd77c0e-44b2-5d46-96fc-9a4120859b5d\",\"version\":1,\"language\":\"en\",\"revision\":\"9ffdc0d9a585441d90bc780ef1c2ca3b\"},\"fieldId\":\"7c7fb943-5c61-51dc-a037-a38b7c9a563b\",\"fieldType\":\"File\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4BD77C0E-44B2-5D46-96FC-9A4120859B5D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"file\",\"expandedDisplayName\":null}" + }, + "heading": { + "value": "File", + "editable": "{\"contextItem\":{\"id\":\"4bd77c0e-44b2-5d46-96fc-9a4120859b5d\",\"version\":1,\"language\":\"en\",\"revision\":\"9ffdc0d9a585441d90bc780ef1c2ca3b\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4BD77C0E-44B2-5D46-96FC-9A4120859B5D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}File" + }, + "description": { + "value": "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n", + "editable": "{\"contextItem\":{\"id\":\"4bd77c0e-44b2-5d46-96fc-9a4120859b5d\",\"version\":1,\"language\":\"en\",\"revision\":\"9ffdc0d9a585441d90bc780ef1c2ca3b\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4BD77C0E-44B2-5D46-96FC-9A4120859B5D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-File", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"be117da9-c86f-563a-a288-bc8e9409a9d0\",\"version\":1,\"language\":\"en\",\"revision\":\"9602ea94999b49bb9ef6a7a75c00dde7\"},\"renderingId\":\"df215418-c334-545f-aba8-07c2da2957db\",\"renderingInstanceId\":\"{FF90D4BD-E50D-5BBF-9213-D25968C9AE75}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|heading|description,id={BE117DA9-C86F-563A-A288-BC8E9409A9D0})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={FF90D4BD-E50D-5BBF-9213-D25968C9AE75},renderingId={DF215418-C334-545F-ABA8-07C2DA2957DB},id={BE117DA9-C86F-563A-A288-BC8E9409A9D0})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={FF90D4BD-E50D-5BBF-9213-D25968C9AE75},renderingId={DF215418-C334-545F-ABA8-07C2DA2957DB},id={BE117DA9-C86F-563A-A288-BC8E9409A9D0})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{BE117DA9-C86F-563A-A288-BC8E9409A9D0}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"DF215418C334545FABA807C2DA2957DB\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Number\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Number", + "id": "r_FF90D4BDE50D5BBF9213D25968C9AE75", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "ff90d4bd-e50d-5bbf-9213-d25968c9ae75", + "componentName": "Styleguide-FieldUsage-Number", + "dataSource": "{BE117DA9-C86F-563A-A288-BC8E9409A9D0}", + "params": {}, + "fields": { + "sample": { + "value": "1.21", + "editable": "{\"contextItem\":{\"id\":\"be117da9-c86f-563a-a288-bc8e9409a9d0\",\"version\":1,\"language\":\"en\",\"revision\":\"9602ea94999b49bb9ef6a7a75c00dde7\"},\"fieldId\":\"668acf0d-2e1d-5600-9f12-cdc742080c90\",\"fieldType\":\"Number\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editnumber\\\"})\",\"header\":\"Edit number\",\"icon\":\"/temp/iconcache/wordprocessing/16x16/word_count.png\",\"disabledIcon\":\"/temp/word_count_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit number\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{BE117DA9-C86F-563A-A288-BC8E9409A9D0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}1.21" + }, + "heading": { + "value": "Number", + "editable": "{\"contextItem\":{\"id\":\"be117da9-c86f-563a-a288-bc8e9409a9d0\",\"version\":1,\"language\":\"en\",\"revision\":\"9602ea94999b49bb9ef6a7a75c00dde7\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{BE117DA9-C86F-563A-A288-BC8E9409A9D0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Number" + }, + "description": { + "value": "Number tells Sitecore to use a number entry for editing.", + "editable": "{\"contextItem\":{\"id\":\"be117da9-c86f-563a-a288-bc8e9409a9d0\",\"version\":1,\"language\":\"en\",\"revision\":\"9602ea94999b49bb9ef6a7a75c00dde7\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{BE117DA9-C86F-563A-A288-BC8E9409A9D0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Number tells Sitecore to use a number entry for editing." + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Number", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"7e243cf3-2355-517a-a88d-df4ee1b51a3b\",\"version\":1,\"language\":\"en\",\"revision\":\"3213d65733db47c2873271fc1a5dc2ea\"},\"renderingId\":\"308b7bec-8922-5fed-848b-22056304212b\",\"renderingInstanceId\":\"{B5C1C74A-A81D-59B2-85D8-09BC109B1F70}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=checkbox|checkbox2|heading|description,id={7E243CF3-2355-517A-A88D-DF4EE1B51A3B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B5C1C74A-A81D-59B2-85D8-09BC109B1F70},renderingId={308B7BEC-8922-5FED-848B-22056304212B},id={7E243CF3-2355-517A-A88D-DF4EE1B51A3B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B5C1C74A-A81D-59B2-85D8-09BC109B1F70},renderingId={308B7BEC-8922-5FED-848B-22056304212B},id={7E243CF3-2355-517A-A88D-DF4EE1B51A3B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"308B7BEC89225FED848B22056304212B\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Checkbox\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Checkbox", + "id": "r_B5C1C74AA81D59B285D809BC109B1F70", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "b5c1c74a-a81d-59b2-85d8-09bc109b1f70", + "componentName": "Styleguide-FieldUsage-Checkbox", + "dataSource": "{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}", + "params": {}, + "fields": { + "checkbox": { + "value": true, + "editable": "{\"contextItem\":{\"id\":\"7e243cf3-2355-517a-a88d-df4ee1b51a3b\",\"version\":1,\"language\":\"en\",\"revision\":\"3213d65733db47c2873271fc1a5dc2ea\"},\"fieldId\":\"489bd3b4-8a4f-5cf9-81a2-0c0196ac0cc6\",\"fieldType\":\"Checkbox\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"checkbox\",\"expandedDisplayName\":null}1" + }, + "checkbox2": { + "value": false, + "editable": "{\"contextItem\":{\"id\":\"7e243cf3-2355-517a-a88d-df4ee1b51a3b\",\"version\":1,\"language\":\"en\",\"revision\":\"3213d65733db47c2873271fc1a5dc2ea\"},\"fieldId\":\"11dafd09-f01d-545e-b0da-83db1a78012d\",\"fieldType\":\"Checkbox\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"checkbox2\",\"expandedDisplayName\":null}0" + }, + "heading": { + "value": "Checkbox", + "editable": "{\"contextItem\":{\"id\":\"7e243cf3-2355-517a-a88d-df4ee1b51a3b\",\"version\":1,\"language\":\"en\",\"revision\":\"3213d65733db47c2873271fc1a5dc2ea\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Checkbox" + }, + "description": { + "value": "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n", + "editable": "{\"contextItem\":{\"id\":\"7e243cf3-2355-517a-a88d-df4ee1b51a3b\",\"version\":1,\"language\":\"en\",\"revision\":\"3213d65733db47c2873271fc1a5dc2ea\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7E243CF3-2355-517A-A88D-DF4EE1B51A3B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Checkbox", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"d4ab53f6-2ca8-5270-80da-0dd414cc1b50\",\"version\":1,\"language\":\"en\",\"revision\":\"4d28f0918b7645a8923a3f9624ec6740\"},\"renderingId\":\"7fc8f8f3-c129-520c-a9fc-43b9f834f816\",\"renderingInstanceId\":\"{F166A7D6-9EC8-5C53-B825-33405DB7F575}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=date|dateTime|heading|description,id={D4AB53F6-2CA8-5270-80DA-0DD414CC1B50})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={F166A7D6-9EC8-5C53-B825-33405DB7F575},renderingId={7FC8F8F3-C129-520C-A9FC-43B9F834F816},id={D4AB53F6-2CA8-5270-80DA-0DD414CC1B50})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={F166A7D6-9EC8-5C53-B825-33405DB7F575},renderingId={7FC8F8F3-C129-520C-A9FC-43B9F834F816},id={D4AB53F6-2CA8-5270-80DA-0DD414CC1B50})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"7FC8F8F3C129520CA9FC43B9F834F816\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Date\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Date", + "id": "r_F166A7D69EC85C53B82533405DB7F575", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "f166a7d6-9ec8-5c53-b825-33405db7f575", + "componentName": "Styleguide-FieldUsage-Date", + "dataSource": "{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}", + "params": {}, + "fields": { + "date": { + "value": "2012-05-04T00:00:00Z", + "editable": "{\"contextItem\":{\"id\":\"d4ab53f6-2ca8-5270-80da-0dd414cc1b50\",\"version\":1,\"language\":\"en\",\"revision\":\"4d28f0918b7645a8923a3f9624ec6740\"},\"fieldId\":\"e0a3c996-5d99-5516-aba1-b683ba7aaf73\",\"fieldType\":\"Date\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editdate\\\"})\",\"header\":\"Show calendar\",\"icon\":\"/temp/iconcache/business/16x16/calendar.png\",\"disabledIcon\":\"/temp/calendar_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Shows the calendar\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:cleardate\\\"})\",\"header\":\"Clear calender\",\"icon\":\"/temp/iconcache/applications/16x16/delete2.png\",\"disabledIcon\":\"/temp/delete2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clear date\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"date\",\"expandedDisplayName\":null}5/4/2012" + }, + "dateTime": { + "value": "2018-03-14T15:00:00Z", + "editable": "{\"contextItem\":{\"id\":\"d4ab53f6-2ca8-5270-80da-0dd414cc1b50\",\"version\":1,\"language\":\"en\",\"revision\":\"4d28f0918b7645a8923a3f9624ec6740\"},\"fieldId\":\"aab05d19-67b4-5936-a1b6-3bc0cfba517d\",\"fieldType\":\"Datetime\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editdatetime\\\"})\",\"header\":\"Show calendar\",\"icon\":\"/temp/iconcache/business/16x16/calendar.png\",\"disabledIcon\":\"/temp/calendar_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Shows the calendar\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:cleardate\\\"})\",\"header\":\"Clear calender\",\"icon\":\"/temp/iconcache/applications/16x16/delete2.png\",\"disabledIcon\":\"/temp/delete2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clear date and time\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"dateTime\",\"expandedDisplayName\":null}3/14/2018 3:00:00 PM" + }, + "heading": { + "value": "Date", + "editable": "{\"contextItem\":{\"id\":\"d4ab53f6-2ca8-5270-80da-0dd414cc1b50\",\"version\":1,\"language\":\"en\",\"revision\":\"4d28f0918b7645a8923a3f9624ec6740\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Date" + }, + "description": { + "value": "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n", + "editable": "{\"contextItem\":{\"id\":\"d4ab53f6-2ca8-5270-80da-0dd414cc1b50\",\"version\":1,\"language\":\"en\",\"revision\":\"4d28f0918b7645a8923a3f9624ec6740\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4AB53F6-2CA8-5270-80DA-0DD414CC1B50}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Date", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"renderingId\":\"de5d5de2-5d71-5629-ab2e-18a4bf4b1d6d\",\"renderingInstanceId\":\"{56A9562A-6813-579B-8ED2-FDDAB1BFD3D2}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=externalLink|internalLink|emailLink|paramsLink|heading|description,id={C5013C89-4D7F-5F46-94D9-207CF5F5938B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={56A9562A-6813-579B-8ED2-FDDAB1BFD3D2},renderingId={DE5D5DE2-5D71-5629-AB2E-18A4BF4B1D6D},id={C5013C89-4D7F-5F46-94D9-207CF5F5938B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={56A9562A-6813-579B-8ED2-FDDAB1BFD3D2},renderingId={DE5D5DE2-5D71-5629-AB2E-18A4BF4B1D6D},id={C5013C89-4D7F-5F46-94D9-207CF5F5938B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"DE5D5DE25D715629AB2E18A4BF4B1D6D\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Link\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Link", + "id": "r_56A9562A6813579B8ED2FDDAB1BFD3D2", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "56a9562a-6813-579b-8ed2-fddab1bfd3d2", + "componentName": "Styleguide-FieldUsage-Link", + "dataSource": "{C5013C89-4D7F-5F46-94D9-207CF5F5938B}", + "params": {}, + "fields": { + "externalLink": { + "value": { + "href": "https://www.sitecore.com", + "text": "Link to Sitecore", + "url": "https://www.sitecore.com", + "linktype": "external" + }, + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"3585656b-abc0-519f-bc91-df4ad323af61\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"externalLink\",\"expandedDisplayName\":null}Link to Sitecore", + "editableFirstPart": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"3585656b-abc0-519f-bc91-df4ad323af61\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"externalLink\",\"expandedDisplayName\":null}Link to Sitecore", + "editableLastPart": "" + }, + "internalLink": { + "value": { + "href": "/en/", + "linktype": "internal", + "id": "{721A5447-6D05-5E9C-989A-58633DACB364}" + }, + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"b92e4269-4998-5cfa-a056-f354341ef3cb\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"internalLink\",\"expandedDisplayName\":null}home", + "editableFirstPart": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"b92e4269-4998-5cfa-a056-f354341ef3cb\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"internalLink\",\"expandedDisplayName\":null}home", + "editableLastPart": "" + }, + "emailLink": { + "value": { + "href": "mailto:foo@bar.com", + "text": "Send an Email", + "url": "mailto:foo@bar.com", + "linktype": "mailto" + }, + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"da405dba-f452-50a2-adfd-0d63cd27f121\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"emailLink\",\"expandedDisplayName\":null}Send an Email", + "editableFirstPart": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"da405dba-f452-50a2-adfd-0d63cd27f121\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"emailLink\",\"expandedDisplayName\":null}Send an Email", + "editableLastPart": "" + }, + "paramsLink": { + "value": { + "href": "https://dev.sitecore.net", + "target": "_blank", + "text": "Sitecore Dev Site", + "title": " title attribute", + "url": "https://dev.sitecore.net", + "class": "font-weight-bold", + "linktype": "external" + }, + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"4f923d0e-98ed-50fe-bd19-d16cd378ad63\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"paramsLink\",\"expandedDisplayName\":null} title attribute\" href=\"https://dev.sitecore.net\" target=\"_blank\">Sitecore Dev Site", + "editableFirstPart": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"4f923d0e-98ed-50fe-bd19-d16cd378ad63\",\"fieldType\":\"General Link\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"paramsLink\",\"expandedDisplayName\":null} title attribute\" href=\"https://dev.sitecore.net\" target=\"_blank\">Sitecore Dev Site", + "editableLastPart": "" + }, + "heading": { + "value": "General Link", + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}General Link" + }, + "description": { + "value": "

A General Link is a field that represents an <a> tag.

", + "editable": "{\"contextItem\":{\"id\":\"c5013c89-4d7f-5f46-94d9-207cf5f5938b\",\"version\":1,\"language\":\"en\",\"revision\":\"8a1fbcfde09f4794969f424a6844cd2b\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C5013C89-4D7F-5F46-94D9-207CF5F5938B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

A General Link is a field that represents an <a> tag.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Link", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"06f150ee-b572-520c-94ba-bc66900bc21d\",\"version\":1,\"language\":\"en\",\"revision\":\"22fc7551b4d2463e90143a3ffa526697\"},\"renderingId\":\"d94f5d1f-b1bb-57c8-a752-dd6a3d283878\",\"renderingInstanceId\":\"{A44AD1F8-0582-5248-9DF9-52429193A68B}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sharedItemLink|localItemLink|heading|description,id={06F150EE-B572-520C-94BA-BC66900BC21D})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A44AD1F8-0582-5248-9DF9-52429193A68B},renderingId={D94F5D1F-B1BB-57C8-A752-DD6A3D283878},id={06F150EE-B572-520C-94BA-BC66900BC21D})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A44AD1F8-0582-5248-9DF9-52429193A68B},renderingId={D94F5D1F-B1BB-57C8-A752-DD6A3D283878},id={06F150EE-B572-520C-94BA-BC66900BC21D})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{06F150EE-B572-520C-94BA-BC66900BC21D}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"D94F5D1FB1BB57C8A752DD6A3D283878\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-ItemLink\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-ItemLink", + "id": "r_A44AD1F8058252489DF952429193A68B", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a44ad1f8-0582-5248-9df9-52429193a68b", + "componentName": "Styleguide-FieldUsage-ItemLink", + "dataSource": "{06F150EE-B572-520C-94BA-BC66900BC21D}", + "params": {}, + "fields": { + "sharedItemLink": { + "id": "9132be52-2c1b-518b-bf0c-3889f9d2be14", + "url": "/test-app-one/Content/Styleguide/ItemLinkField/Item1", + "fields": { + "textField": { + "value": "ItemLink Demo (Shared) Item 1 Text Field", + "editable": "{\"contextItem\":{\"id\":\"9132be52-2c1b-518b-bf0c-3889f9d2be14\",\"version\":1,\"language\":\"en\",\"revision\":\"6ccd2c8008fa4b29aa5c52536de44f4a\"},\"fieldId\":\"af6d9e2e-fb4a-5478-b8e1-4301b3ec4b4e\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9132BE52-2C1B-518B-BF0C-3889F9D2BE14}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ItemLink Demo (Shared) Item 1 Text Field" + } + } + }, + "localItemLink": { + "id": "419b9be6-b9aa-50fb-8749-50899f5a8fea", + "url": "/styleguide/Page-Components/styleguide-jss-styleguide-section-B73482E131E5A083D77A50554BC74A4758E29636DF6824F6E2F272EE778C28A095/styleguide-jss-styleguide-section-B75151F05CFDC4CAFFE44E5BAED9D59BEA82565EC11CE75B7DEF3634495EC1DAB7", + "fields": { + "textField": { + "value": "Referenced item textField", + "editable": "{\"contextItem\":{\"id\":\"419b9be6-b9aa-50fb-8749-50899f5a8fea\",\"version\":1,\"language\":\"en\",\"revision\":\"b70b3646eb0c46dd88a6892ff8047e9e\"},\"fieldId\":\"af6d9e2e-fb4a-5478-b8e1-4301b3ec4b4e\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{419B9BE6-B9AA-50FB-8749-50899F5A8FEA}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Referenced item textField" + } + } + }, + "heading": { + "value": "Item Link", + "editable": "{\"contextItem\":{\"id\":\"06f150ee-b572-520c-94ba-bc66900bc21d\",\"version\":1,\"language\":\"en\",\"revision\":\"22fc7551b4d2463e90143a3ffa526697\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{06F150EE-B572-520C-94BA-BC66900BC21D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Item Link" + }, + "description": { + "value": "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n", + "editable": "{\"contextItem\":{\"id\":\"06f150ee-b572-520c-94ba-bc66900bc21d\",\"version\":1,\"language\":\"en\",\"revision\":\"22fc7551b4d2463e90143a3ffa526697\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{06F150EE-B572-520C-94BA-BC66900BC21D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-ItemLink", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"59eec001-2fe0-5d25-ae2f-214e21461321\",\"version\":1,\"language\":\"en\",\"revision\":\"78f03758f545451b856d49b3c041375b\"},\"renderingId\":\"84b83365-2af8-5a79-b719-e5381bf8cdef\",\"renderingInstanceId\":\"{2F609D40-8AD9-540E-901E-23AA2600F3EB}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sharedContentList|localContentList|heading|description,id={59EEC001-2FE0-5D25-AE2F-214E21461321})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={2F609D40-8AD9-540E-901E-23AA2600F3EB},renderingId={84B83365-2AF8-5A79-B719-E5381BF8CDEF},id={59EEC001-2FE0-5D25-AE2F-214E21461321})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={2F609D40-8AD9-540E-901E-23AA2600F3EB},renderingId={84B83365-2AF8-5A79-B719-E5381BF8CDEF},id={59EEC001-2FE0-5D25-AE2F-214E21461321})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{59EEC001-2FE0-5D25-AE2F-214E21461321}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"84B833652AF85A79B719E5381BF8CDEF\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-ContentList\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-ContentList", + "id": "r_2F609D408AD9540E901E23AA2600F3EB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "2f609d40-8ad9-540e-901e-23aa2600f3eb", + "componentName": "Styleguide-FieldUsage-ContentList", + "dataSource": "{59EEC001-2FE0-5D25-AE2F-214E21461321}", + "params": {}, + "fields": { + "sharedContentList": [ + { + "id": "7c7f58c4-735b-59c5-8478-f9a8578a79dc", + "fields": { + "textField": { + "value": "ContentList Demo (Shared) Item 1 Text Field", + "editable": "{\"contextItem\":{\"id\":\"7c7f58c4-735b-59c5-8478-f9a8578a79dc\",\"version\":1,\"language\":\"en\",\"revision\":\"226f1574eb67405cb5db1f9c847092c9\"},\"fieldId\":\"b4fff353-256b-5f8a-a792-7f5253df74ed\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7C7F58C4-735B-59C5-8478-F9A8578A79DC}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ContentList Demo (Shared) Item 1 Text Field" + } + } + }, + { + "id": "a1340fa4-5c87-5038-aa2c-4360ecb271a9", + "fields": { + "textField": { + "value": "ContentList Demo (Shared) Item 2 Text Field", + "editable": "{\"contextItem\":{\"id\":\"a1340fa4-5c87-5038-aa2c-4360ecb271a9\",\"version\":1,\"language\":\"en\",\"revision\":\"421b8ccddee743b8b2fddf88d0069474\"},\"fieldId\":\"b4fff353-256b-5f8a-a792-7f5253df74ed\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A1340FA4-5C87-5038-AA2C-4360ECB271A9}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ContentList Demo (Shared) Item 2 Text Field" + } + } + } + ], + "localContentList": [ + { + "id": "1d9581cf-caad-5803-bf14-b5e2debfd1ae", + "fields": { + "textField": { + "value": "Hello World Item 1", + "editable": "{\"contextItem\":{\"id\":\"1d9581cf-caad-5803-bf14-b5e2debfd1ae\",\"version\":1,\"language\":\"en\",\"revision\":\"19995490d5b04047a7dab1dcc0500741\"},\"fieldId\":\"b4fff353-256b-5f8a-a792-7f5253df74ed\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1D9581CF-CAAD-5803-BF14-B5E2DEBFD1AE}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Hello World Item 1" + } + } + }, + { + "id": "f8195a6f-edf2-55b0-87ae-ee090fbec764", + "fields": { + "textField": { + "value": "Hello World Item 2", + "editable": "{\"contextItem\":{\"id\":\"f8195a6f-edf2-55b0-87ae-ee090fbec764\",\"version\":1,\"language\":\"en\",\"revision\":\"93c684d3577e49678515239bfb9a79b6\"},\"fieldId\":\"b4fff353-256b-5f8a-a792-7f5253df74ed\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F8195A6F-EDF2-55B0-87AE-EE090FBEC764}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Hello World Item 2" + } + } + } + ], + "heading": { + "value": "Content List", + "editable": "{\"contextItem\":{\"id\":\"59eec001-2fe0-5d25-ae2f-214e21461321\",\"version\":1,\"language\":\"en\",\"revision\":\"78f03758f545451b856d49b3c041375b\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{59EEC001-2FE0-5D25-AE2F-214E21461321}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Content List" + }, + "description": { + "value": "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n", + "editable": "{\"contextItem\":{\"id\":\"59eec001-2fe0-5d25-ae2f-214e21461321\",\"version\":1,\"language\":\"en\",\"revision\":\"78f03758f545451b856d49b3c041375b\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{59EEC001-2FE0-5D25-AE2F-214E21461321}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-ContentList", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"9166272b-a00a-5106-88f9-08541bc3ab29\",\"version\":1,\"language\":\"en\",\"revision\":\"6562fe15a4b74b50881d9cfd973e306a\"},\"renderingId\":\"a9c5faf6-c0ef-5e32-a0e1-a056393786ac\",\"renderingInstanceId\":\"{352ED63D-796A-5523-89F5-9A991DDA4A8F}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=customIntField|heading|description,id={9166272B-A00A-5106-88F9-08541BC3AB29})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={352ED63D-796A-5523-89F5-9A991DDA4A8F},renderingId={A9C5FAF6-C0EF-5E32-A0E1-A056393786AC},id={9166272B-A00A-5106-88F9-08541BC3AB29})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={352ED63D-796A-5523-89F5-9A991DDA4A8F},renderingId={A9C5FAF6-C0EF-5E32-A0E1-A056393786AC},id={9166272B-A00A-5106-88F9-08541BC3AB29})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9166272B-A00A-5106-88F9-08541BC3AB29}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A9C5FAF6C0EF5E32A0E1A056393786AC\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Custom\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Custom", + "id": "r_352ED63D796A552389F59A991DDA4A8F", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "352ed63d-796a-5523-89f5-9a991dda4a8f", + "componentName": "Styleguide-FieldUsage-Custom", + "dataSource": "{9166272B-A00A-5106-88F9-08541BC3AB29}", + "params": {}, + "fields": { + "customIntField": { + "value": "31337", + "editable": "{\"contextItem\":{\"id\":\"9166272b-a00a-5106-88f9-08541bc3ab29\",\"version\":1,\"language\":\"en\",\"revision\":\"6562fe15a4b74b50881d9cfd973e306a\"},\"fieldId\":\"76143780-74e3-5dba-a8ac-03c28e6cb3f2\",\"fieldType\":\"Integer\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editinteger\\\"})\",\"header\":\"Edit integer\",\"icon\":\"/temp/iconcache/wordprocessing/16x16/word_count.png\",\"disabledIcon\":\"/temp/word_count_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit integer\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9166272B-A00A-5106-88F9-08541BC3AB29}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"customIntField\",\"expandedDisplayName\":null}31337" + }, + "heading": { + "value": "Custom Fields", + "editable": "{\"contextItem\":{\"id\":\"9166272b-a00a-5106-88f9-08541bc3ab29\",\"version\":1,\"language\":\"en\",\"revision\":\"6562fe15a4b74b50881d9cfd973e306a\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9166272B-A00A-5106-88F9-08541BC3AB29}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Custom Fields" + }, + "description": { + "value": "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n", + "editable": "{\"contextItem\":{\"id\":\"9166272b-a00a-5106-88f9-08541bc3ab29\",\"version\":1,\"language\":\"en\",\"revision\":\"6562fe15a4b74b50881d9cfd973e306a\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{9166272B-A00A-5106-88F9-08541BC3AB29}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Custom", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"10d03889-8865-5841-93d3-83ef6f3c235e\",\"version\":1,\"language\":\"en\",\"revision\":\"e3e34fa0a1d1449c944e08ef9157f262\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={10D03889-8865-5841-93D3-83EF6F3C235E})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7DE41A1A-24E4-5963-8206-3BB0B7D9DD69},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={10D03889-8865-5841-93D3-83EF6F3C235E})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7DE41A1A-24E4-5963-8206-3BB0B7D9DD69},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={10D03889-8865-5841-93D3-83EF6F3C235E})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{10D03889-8865-5841-93D3-83EF6F3C235E}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_7DE41A1A24E4596382063BB0B7D9DD69", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7de41a1a-24e4-5963-8206-3bb0b7d9dd69", + "componentName": "Styleguide-Section", + "dataSource": "{10D03889-8865-5841-93D3-83EF6F3C235E}", + "params": {}, + "fields": { + "heading": { + "value": "Layout Patterns", + "editable": "{\"contextItem\":{\"id\":\"10d03889-8865-5841-93d3-83ef6f3c235e\",\"version\":1,\"language\":\"en\",\"revision\":\"e3e34fa0a1d1449c944e08ef9157f262\"},\"fieldId\":\"ac4286bb-8f0f-5ba1-b2d1-fca768658290\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{10D03889-8865-5841-93D3-83EF6F3C235E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Layout Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section\",\"jss-styleguide-section\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"76FB99EF095B513DAA99CF0417582C61\",\"CE8CE858ED84594FAADC25691D673893\",\"5D5AA98C7F85542BBA008A760B25C350\",\"DF215418C334545FABA807C2DA2957DB\",\"308B7BEC89225FED848B22056304212B\",\"7FC8F8F3C129520CA9FC43B9F834F816\",\"DE5D5DE25D715629AB2E18A4BF4B1D6D\",\"D94F5D1FB1BB57C8A752DD6A3D283878\",\"84B833652AF85A79B719E5381BF8CDEF\",\"A9C5FAF6C0EF5E32A0E1A056393786AC\",\"D92203380E605AB6B894BABC4F9DC5B9\",\"B592151D5AD0513D8037440F550D9D4C\",\"1B482CC5C9015A46BC42444C31A3AC60\",\"4D93967180FB50D0AAF5FCF281CE5F4F\",\"A9E1268B25DA5D1EA5C771B410D32E4D\",\"C24FDF955E4150A9BD289279D40E259B\",\"18A84B80BE55538F827E4E55970138A1\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"76330e9b-79c7-5c2f-8495-5da9cdb5acaf\",\"version\":1,\"language\":\"en\",\"revision\":\"e321992182bc45fea39e65393d2903ca\"},\"renderingId\":\"d9220338-0e60-5ab6-b894-babc4f9dc5b9\",\"renderingInstanceId\":\"{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A},renderingId={D9220338-0E60-5AB6-B894-BABC4F9DC5B9},id={76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A},renderingId={D9220338-0E60-5AB6-B894-BABC4F9DC5B9},id={76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"D92203380E605AB6B894BABC4F9DC5B9\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Reuse\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Reuse", + "id": "r_3A5D9C50D8C15A128DA85D56C2A5A69A", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "3a5d9c50-d8c1-5a12-8da8-5d56c2a5a69a", + "componentName": "Styleguide-Layout-Reuse", + "dataSource": "{76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF}", + "params": {}, + "fields": { + "heading": { + "value": "Reusing Content", + "editable": "{\"contextItem\":{\"id\":\"76330e9b-79c7-5c2f-8495-5da9cdb5acaf\",\"version\":1,\"language\":\"en\",\"revision\":\"e321992182bc45fea39e65393d2903ca\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Reusing Content" + }, + "description": { + "value": "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

", + "editable": "{\"contextItem\":{\"id\":\"76330e9b-79c7-5c2f-8495-5da9cdb5acaf\",\"version\":1,\"language\":\"en\",\"revision\":\"e321992182bc45fea39e65393d2903ca\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{76330E9B-79C7-5C2F-8495-5DA9CDB5ACAF}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

" + } + }, + "placeholders": { + "jss-reuse-example": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-reuse-example-{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-reuse-example\",\"jss-reuse-example\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"71AAF5C592425806BE7CECDF9154840B\"],\"editable\":\"true\"},\"displayName\":\"jss-reuse-example\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0_jss_reuse_example__3A5D9C50_D8C1_5A12_8DA8_5D56C2A5A69A__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-reuse-example-{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{AA328B8A-D6E1-5B37-8143-250D2E93D6B8}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={AA328B8A-D6E1-5B37-8143-250D2E93D6B8},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={AA328B8A-D6E1-5B37-8143-250D2E93D6B8},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_AA328B8AD6E15B378143250D2E93D6B8", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "aa328b8a-d6e1-5b37-8143-250d2e93d6b8", + "componentName": "ContentBlock", + "dataSource": "{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}", + "params": {}, + "fields": { + "heading": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"fieldId\":\"3f8e6ee3-8678-567b-a1fc-e5edb276cca3\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{C4330D34-623C-556C-BF4C-97C93D40FB1E}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={C4330D34-623C-556C-BF4C-97C93D40FB1E},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={C4330D34-623C-556C-BF4C-97C93D40FB1E},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_C4330D34623C556CBF4C97C93D40FB1E", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "c4330d34-623c-556c-bf4c-97c93d40fb1e", + "componentName": "ContentBlock", + "dataSource": "{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}", + "params": {}, + "fields": { + "heading": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"fieldId\":\"3f8e6ee3-8678-567b-a1fc-e5edb276cca3\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"contextItem\":{\"id\":\"ce9c49c8-8df1-5be6-b98d-6155c7140a8f\",\"version\":1,\"language\":\"en\",\"revision\":\"9a0e978afbef4010a57277a063087bce\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{CE9C49C8-8DF1-5BE6-B98D-6155C7140A8F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"988825af-57c3-51f3-afd2-ba4ad8f56934\",\"version\":1,\"language\":\"en\",\"revision\":\"0d8d93193d6c402d895362f92b6a6ce4\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{A42D8B1C-193D-5627-9130-F7F7F87617F1}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={988825AF-57C3-51F3-AFD2-BA4AD8F56934})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A42D8B1C-193D-5627-9130-F7F7F87617F1},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={988825AF-57C3-51F3-AFD2-BA4AD8F56934})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A42D8B1C-193D-5627-9130-F7F7F87617F1},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={988825AF-57C3-51F3-AFD2-BA4AD8F56934})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{988825AF-57C3-51F3-AFD2-BA4AD8F56934}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_A42D8B1C193D56279130F7F7F87617F1", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a42d8b1c-193d-5627-9130-f7f7f87617f1", + "componentName": "ContentBlock", + "dataSource": "{988825AF-57C3-51F3-AFD2-BA4AD8F56934}", + "params": {}, + "fields": { + "heading": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"988825af-57c3-51f3-afd2-ba4ad8f56934\",\"version\":1,\"language\":\"en\",\"revision\":\"0d8d93193d6c402d895362f92b6a6ce4\"},\"fieldId\":\"3f8e6ee3-8678-567b-a1fc-e5edb276cca3\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{988825AF-57C3-51F3-AFD2-BA4AD8F56934}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"contextItem\":{\"id\":\"988825af-57c3-51f3-afd2-ba4ad8f56934\",\"version\":1,\"language\":\"en\",\"revision\":\"0d8d93193d6c402d895362f92b6a6ce4\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{988825AF-57C3-51F3-AFD2-BA4AD8F56934}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"b793842c-01e4-508d-8206-604da4e80538\",\"version\":1,\"language\":\"en\",\"revision\":\"f527669a3ea3439b9c0532c1e6d53692\"},\"renderingId\":\"71aaf5c5-9242-5806-be7c-ecdf9154840b\",\"renderingInstanceId\":\"{0F4CB47A-979E-5139-B50B-A8E40C73C236}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={B793842C-01E4-508D-8206-604DA4E80538})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={0F4CB47A-979E-5139-B50B-A8E40C73C236},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={B793842C-01E4-508D-8206-604DA4E80538})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={0F4CB47A-979E-5139-B50B-A8E40C73C236},renderingId={71AAF5C5-9242-5806-BE7C-ECDF9154840B},id={B793842C-01E4-508D-8206-604DA4E80538})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B793842C-01E4-508D-8206-604DA4E80538}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"71AAF5C592425806BE7CECDF9154840B\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_0F4CB47A979E5139B50BA8E40C73C236", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "0f4cb47a-979e-5139-b50b-a8e40c73c236", + "componentName": "ContentBlock", + "dataSource": "{B793842C-01E4-508D-8206-604DA4E80538}", + "params": {}, + "fields": { + "heading": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"b793842c-01e4-508d-8206-604da4e80538\",\"version\":1,\"language\":\"en\",\"revision\":\"f527669a3ea3439b9c0532c1e6d53692\"},\"fieldId\":\"3f8e6ee3-8678-567b-a1fc-e5edb276cca3\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B793842C-01E4-508D-8206-604DA4E80538}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

", + "editable": "{\"contextItem\":{\"id\":\"b793842c-01e4-508d-8206-604da4e80538\",\"version\":1,\"language\":\"en\",\"revision\":\"f527669a3ea3439b9c0532c1e6d53692\"},\"fieldId\":\"6856af27-b413-5fce-b3fd-c560612f1199\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B793842C-01E4-508D-8206-604DA4E80538}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-reuse-example", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Reuse", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8bd52ae0-1102-5123-bdbd-9fc9ce11150a\",\"version\":1,\"language\":\"en\",\"revision\":\"356a93955b644a4692c017b541cbc494\"},\"renderingId\":\"b592151d-5ad0-513d-8037-440f550d9d4c\",\"renderingInstanceId\":\"{538E4831-F157-50BB-AC74-277FCAC9FDDB}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={8BD52AE0-1102-5123-BDBD-9FC9CE11150A})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={538E4831-F157-50BB-AC74-277FCAC9FDDB},renderingId={B592151D-5AD0-513D-8037-440F550D9D4C},id={8BD52AE0-1102-5123-BDBD-9FC9CE11150A})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={538E4831-F157-50BB-AC74-277FCAC9FDDB},renderingId={B592151D-5AD0-513D-8037-440F550D9D4C},id={8BD52AE0-1102-5123-BDBD-9FC9CE11150A})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8BD52AE0-1102-5123-BDBD-9FC9CE11150A}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"B592151D5AD0513D8037440F550D9D4C\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs", + "id": "r_538E4831F15750BBAC74277FCAC9FDDB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "538e4831-f157-50bb-ac74-277fcac9fddb", + "componentName": "Styleguide-Layout-Tabs", + "dataSource": "{8BD52AE0-1102-5123-BDBD-9FC9CE11150A}", + "params": {}, + "fields": { + "heading": { + "value": "Tabs", + "editable": "{\"contextItem\":{\"id\":\"8bd52ae0-1102-5123-bdbd-9fc9ce11150a\",\"version\":1,\"language\":\"en\",\"revision\":\"356a93955b644a4692c017b541cbc494\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8BD52AE0-1102-5123-BDBD-9FC9CE11150A}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Tabs" + }, + "description": { + "value": "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

", + "editable": "{\"contextItem\":{\"id\":\"8bd52ae0-1102-5123-bdbd-9fc9ce11150a\",\"version\":1,\"language\":\"en\",\"revision\":\"356a93955b644a4692c017b541cbc494\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8BD52AE0-1102-5123-BDBD-9FC9CE11150A}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

" + } + }, + "placeholders": { + "jss-tabs": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-tabs-{538E4831-F157-50BB-AC74-277FCAC9FDDB}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-tabs\",\"jss-tabs\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"8BCB1F9E8E97519E86D80DE44A7F0F9A\"],\"editable\":\"true\"},\"displayName\":\"Tabs\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0_jss_tabs__538E4831_F157_50BB_AC74_277FCAC9FDDB__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-tabs-{538E4831-F157-50BB-AC74-277FCAC9FDDB}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"c9ba49e7-ab96-56bd-a70d-27ea1e5b30d9\",\"version\":1,\"language\":\"en\",\"revision\":\"7d503f9ef3414f96b621708321ca4040\"},\"renderingId\":\"8bcb1f9e-8e97-519e-86d8-0de44a7f0f9a\",\"renderingInstanceId\":\"{7ECB2ED2-AC9B-58D1-8365-10CA74824AF7}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7ECB2ED2-AC9B-58D1-8365-10CA74824AF7},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7ECB2ED2-AC9B-58D1-8365-10CA74824AF7},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"8BCB1F9E8E97519E86D80DE44A7F0F9A\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_7ECB2ED2AC9B58D1836510CA74824AF7", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7ecb2ed2-ac9b-58d1-8365-10ca74824af7", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9}", + "params": {}, + "fields": { + "title": { + "value": "Tab 1", + "editable": "{\"contextItem\":{\"id\":\"c9ba49e7-ab96-56bd-a70d-27ea1e5b30d9\",\"version\":1,\"language\":\"en\",\"revision\":\"7d503f9ef3414f96b621708321ca4040\"},\"fieldId\":\"2d488f66-163d-5c46-83ed-ba2e3c63a8e1\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 1" + }, + "content": { + "value": "

Tab 1 contents!

", + "editable": "{\"contextItem\":{\"id\":\"c9ba49e7-ab96-56bd-a70d-27ea1e5b30d9\",\"version\":1,\"language\":\"en\",\"revision\":\"7d503f9ef3414f96b621708321ca4040\"},\"fieldId\":\"b5dac71c-9bb9-561d-944c-1f5fb1ade7be\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C9BA49E7-AB96-56BD-A70D-27EA1E5B30D9}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 1 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"91bb2a6c-4eee-5bbc-bbf5-0381157ce93e\",\"version\":1,\"language\":\"en\",\"revision\":\"c7fa31652d094c8aaecc41cd685ae5f6\"},\"renderingId\":\"8bcb1f9e-8e97-519e-86d8-0de44a7f0f9a\",\"renderingInstanceId\":\"{AFD64900-0A61-50EB-A674-A7A884E0D496}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={AFD64900-0A61-50EB-A674-A7A884E0D496},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={AFD64900-0A61-50EB-A674-A7A884E0D496},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"8BCB1F9E8E97519E86D80DE44A7F0F9A\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_AFD649000A6150EBA674A7A884E0D496", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "afd64900-0a61-50eb-a674-a7a884e0d496", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E}", + "params": {}, + "fields": { + "title": { + "value": "Tab 2", + "editable": "{\"contextItem\":{\"id\":\"91bb2a6c-4eee-5bbc-bbf5-0381157ce93e\",\"version\":1,\"language\":\"en\",\"revision\":\"c7fa31652d094c8aaecc41cd685ae5f6\"},\"fieldId\":\"2d488f66-163d-5c46-83ed-ba2e3c63a8e1\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 2" + }, + "content": { + "value": "

Tab 2 contents!

", + "editable": "{\"contextItem\":{\"id\":\"91bb2a6c-4eee-5bbc-bbf5-0381157ce93e\",\"version\":1,\"language\":\"en\",\"revision\":\"c7fa31652d094c8aaecc41cd685ae5f6\"},\"fieldId\":\"b5dac71c-9bb9-561d-944c-1f5fb1ade7be\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{91BB2A6C-4EEE-5BBC-BBF5-0381157CE93E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 2 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"6e37da11-36e5-5215-b307-1aa5a6d883ef\",\"version\":1,\"language\":\"en\",\"revision\":\"b92edc715e054079b6e128f658d9c917\"},\"renderingId\":\"8bcb1f9e-8e97-519e-86d8-0de44a7f0f9a\",\"renderingInstanceId\":\"{44C12983-3A84-5462-84C0-6CA1430050C8}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={6E37DA11-36E5-5215-B307-1AA5A6D883EF})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={44C12983-3A84-5462-84C0-6CA1430050C8},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={6E37DA11-36E5-5215-B307-1AA5A6D883EF})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={44C12983-3A84-5462-84C0-6CA1430050C8},renderingId={8BCB1F9E-8E97-519E-86D8-0DE44A7F0F9A},id={6E37DA11-36E5-5215-B307-1AA5A6D883EF})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6E37DA11-36E5-5215-B307-1AA5A6D883EF}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"8BCB1F9E8E97519E86D80DE44A7F0F9A\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_44C129833A84546284C06CA1430050C8", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "44c12983-3a84-5462-84c0-6ca1430050c8", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{6E37DA11-36E5-5215-B307-1AA5A6D883EF}", + "params": {}, + "fields": { + "title": { + "value": "Tab 3", + "editable": "{\"contextItem\":{\"id\":\"6e37da11-36e5-5215-b307-1aa5a6d883ef\",\"version\":1,\"language\":\"en\",\"revision\":\"b92edc715e054079b6e128f658d9c917\"},\"fieldId\":\"2d488f66-163d-5c46-83ed-ba2e3c63a8e1\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6E37DA11-36E5-5215-B307-1AA5A6D883EF}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 3" + }, + "content": { + "value": "

Tab 3 contents!

", + "editable": "{\"contextItem\":{\"id\":\"6e37da11-36e5-5215-b307-1aa5a6d883ef\",\"version\":1,\"language\":\"en\",\"revision\":\"b92edc715e054079b6e128f658d9c917\"},\"fieldId\":\"b5dac71c-9bb9-561d-944c-1f5fb1ade7be\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6E37DA11-36E5-5215-B307-1AA5A6D883EF}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 3 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "Tabs", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"3d368f70-ec4f-5eb4-87d2-41ee8b7bfd3a\",\"version\":1,\"language\":\"en\",\"revision\":\"946b1bfaf5b9487791508ac0853c8dde\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{2D806C25-DD46-51E3-93DE-63CF9035122C}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={2D806C25-DD46-51E3-93DE-63CF9035122C},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={2D806C25-DD46-51E3-93DE-63CF9035122C},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_2D806C25DD4651E393DE63CF9035122C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "2d806c25-dd46-51e3-93de-63cf9035122c", + "componentName": "Styleguide-Section", + "dataSource": "{3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A}", + "params": {}, + "fields": { + "heading": { + "value": "Sitecore Patterns", + "editable": "{\"contextItem\":{\"id\":\"3d368f70-ec4f-5eb4-87d2-41ee8b7bfd3a\",\"version\":1,\"language\":\"en\",\"revision\":\"946b1bfaf5b9487791508ac0853c8dde\"},\"fieldId\":\"ac4286bb-8f0f-5ba1-b2d1-fca768658290\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{3D368F70-EC4F-5EB4-87D2-41EE8B7BFD3A}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Sitecore Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{2D806C25-DD46-51E3-93DE-63CF9035122C}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section\",\"jss-styleguide-section\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"76FB99EF095B513DAA99CF0417582C61\",\"CE8CE858ED84594FAADC25691D673893\",\"5D5AA98C7F85542BBA008A760B25C350\",\"DF215418C334545FABA807C2DA2957DB\",\"308B7BEC89225FED848B22056304212B\",\"7FC8F8F3C129520CA9FC43B9F834F816\",\"DE5D5DE25D715629AB2E18A4BF4B1D6D\",\"D94F5D1FB1BB57C8A752DD6A3D283878\",\"84B833652AF85A79B719E5381BF8CDEF\",\"A9C5FAF6C0EF5E32A0E1A056393786AC\",\"D92203380E605AB6B894BABC4F9DC5B9\",\"B592151D5AD0513D8037440F550D9D4C\",\"1B482CC5C9015A46BC42444C31A3AC60\",\"4D93967180FB50D0AAF5FCF281CE5F4F\",\"A9E1268B25DA5D1EA5C771B410D32E4D\",\"C24FDF955E4150A9BD289279D40E259B\",\"18A84B80BE55538F827E4E55970138A1\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__2D806C25_DD46_51E3_93DE_63CF9035122C__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{2D806C25-DD46-51E3-93DE-63CF9035122C}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8276ef1a-b0be-517f-a23e-e7c819d1067f\",\"version\":1,\"language\":\"en\",\"revision\":\"349b1ae09f844e6ebb590b554ee8368f\"},\"renderingId\":\"1b482cc5-c901-5a46-bc42-444c31a3ac60\",\"renderingInstanceId\":\"{471FA16A-BB82-5C42-9C95-E7EAB1E3BD30}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={8276EF1A-B0BE-517F-A23E-E7C819D1067F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={471FA16A-BB82-5C42-9C95-E7EAB1E3BD30},renderingId={1B482CC5-C901-5A46-BC42-444C31A3AC60},id={8276EF1A-B0BE-517F-A23E-E7C819D1067F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={471FA16A-BB82-5C42-9C95-E7EAB1E3BD30},renderingId={1B482CC5-C901-5A46-BC42-444C31A3AC60},id={8276EF1A-B0BE-517F-A23E-E7C819D1067F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8276EF1A-B0BE-517F-A23E-E7C819D1067F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1B482CC5C9015A46BC42444C31A3AC60\",\"editable\":\"true\"},\"displayName\":\"Styleguide-SitecoreContext\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-SitecoreContext", + "id": "r_471FA16ABB825C429C95E7EAB1E3BD30", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "471fa16a-bb82-5c42-9c95-e7eab1e3bd30", + "componentName": "Styleguide-SitecoreContext", + "dataSource": "{8276EF1A-B0BE-517F-A23E-E7C819D1067F}", + "params": {}, + "fields": { + "heading": { + "value": "Sitecore Context", + "editable": "{\"contextItem\":{\"id\":\"8276ef1a-b0be-517f-a23e-e7c819d1067f\",\"version\":1,\"language\":\"en\",\"revision\":\"349b1ae09f844e6ebb590b554ee8368f\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8276EF1A-B0BE-517F-A23E-E7C819D1067F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Sitecore Context" + }, + "description": { + "value": "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

", + "editable": "{\"contextItem\":{\"id\":\"8276ef1a-b0be-517f-a23e-e7c819d1067f\",\"version\":1,\"language\":\"en\",\"revision\":\"349b1ae09f844e6ebb590b554ee8368f\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{8276EF1A-B0BE-517F-A23E-E7C819D1067F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-SitecoreContext", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"539b1399-de05-5f12-97eb-3bb67a9a2e94\",\"version\":1,\"language\":\"en\",\"revision\":\"07ca28de26d949aa8ba8259161b5cc38\"},\"renderingId\":\"4d939671-80fb-50d0-aaf5-fcf281ce5f4f\",\"renderingInstanceId\":\"{21F21053-8F8A-5436-BC79-E674E246A2FC}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={539B1399-DE05-5F12-97EB-3BB67A9A2E94})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={21F21053-8F8A-5436-BC79-E674E246A2FC},renderingId={4D939671-80FB-50D0-AAF5-FCF281CE5F4F},id={539B1399-DE05-5F12-97EB-3BB67A9A2E94})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={21F21053-8F8A-5436-BC79-E674E246A2FC},renderingId={4D939671-80FB-50D0-AAF5-FCF281CE5F4F},id={539B1399-DE05-5F12-97EB-3BB67A9A2E94})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{539B1399-DE05-5F12-97EB-3BB67A9A2E94}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"4D93967180FB50D0AAF5FCF281CE5F4F\",\"editable\":\"true\"},\"displayName\":\"Styleguide-RouteFields\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-RouteFields", + "id": "r_21F210538F8A5436BC79E674E246A2FC", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "21f21053-8f8a-5436-bc79-e674e246a2fc", + "componentName": "Styleguide-RouteFields", + "dataSource": "{539B1399-DE05-5F12-97EB-3BB67A9A2E94}", + "params": {}, + "fields": { + "heading": { + "value": "Route-level Fields", + "editable": "{\"contextItem\":{\"id\":\"539b1399-de05-5f12-97eb-3bb67a9a2e94\",\"version\":1,\"language\":\"en\",\"revision\":\"07ca28de26d949aa8ba8259161b5cc38\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{539B1399-DE05-5F12-97EB-3BB67A9A2E94}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Route-level Fields" + }, + "description": { + "value": "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

", + "editable": "{\"contextItem\":{\"id\":\"539b1399-de05-5f12-97eb-3bb67a9a2e94\",\"version\":1,\"language\":\"en\",\"revision\":\"07ca28de26d949aa8ba8259161b5cc38\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{539B1399-DE05-5F12-97EB-3BB67A9A2E94}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-RouteFields", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"b40422e8-f1af-5604-8bb3-fce6f79c0f89\",\"version\":1,\"language\":\"en\",\"revision\":\"8c99a7845e7b4fb782b94a5f6fc016b1\"},\"renderingId\":\"a9e1268b-25da-5d1e-a5c7-71b410d32e4d\",\"renderingInstanceId\":\"{A0A66136-C21F-52E8-A2EA-F04DCFA6A027}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={B40422E8-F1AF-5604-8BB3-FCE6F79C0F89})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A0A66136-C21F-52E8-A2EA-F04DCFA6A027},renderingId={A9E1268B-25DA-5D1E-A5C7-71B410D32E4D},id={B40422E8-F1AF-5604-8BB3-FCE6F79C0F89})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A0A66136-C21F-52E8-A2EA-F04DCFA6A027},renderingId={A9E1268B-25DA-5D1E-A5C7-71B410D32E4D},id={B40422E8-F1AF-5604-8BB3-FCE6F79C0F89})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B40422E8-F1AF-5604-8BB3-FCE6F79C0F89}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A9E1268B25DA5D1EA5C771B410D32E4D\",\"editable\":\"true\"},\"displayName\":\"Styleguide-ComponentParams\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-ComponentParams", + "id": "r_A0A66136C21F52E8A2EAF04DCFA6A027", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a0a66136-c21f-52e8-a2ea-f04dcfa6a027", + "componentName": "Styleguide-ComponentParams", + "dataSource": "{B40422E8-F1AF-5604-8BB3-FCE6F79C0F89}", + "params": { + "cssClass": "alert alert-success", + "columns": "5", + "useCallToAction": "true" + }, + "fields": { + "heading": { + "value": "Component Params", + "editable": "{\"contextItem\":{\"id\":\"b40422e8-f1af-5604-8bb3-fce6f79c0f89\",\"version\":1,\"language\":\"en\",\"revision\":\"8c99a7845e7b4fb782b94a5f6fc016b1\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B40422E8-F1AF-5604-8BB3-FCE6F79C0F89}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Component Params" + }, + "description": { + "value": "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

", + "editable": "{\"contextItem\":{\"id\":\"b40422e8-f1af-5604-8bb3-fce6f79c0f89\",\"version\":1,\"language\":\"en\",\"revision\":\"8c99a7845e7b4fb782b94a5f6fc016b1\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B40422E8-F1AF-5604-8BB3-FCE6F79C0F89}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-ComponentParams", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"047663db-23c2-523c-832e-0e95fbb244b8\",\"version\":1,\"language\":\"en\",\"revision\":\"110ff663bb8d44edb7e2d4f248a60870\"},\"renderingId\":\"c24fdf95-5e41-50a9-bd28-9279d40e259b\",\"renderingInstanceId\":\"{7F765FCB-3B10-58FD-8AA7-B346EF38C9BB}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={047663DB-23C2-523C-832E-0E95FBB244B8})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7F765FCB-3B10-58FD-8AA7-B346EF38C9BB},renderingId={C24FDF95-5E41-50A9-BD28-9279D40E259B},id={047663DB-23C2-523C-832E-0E95FBB244B8})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7F765FCB-3B10-58FD-8AA7-B346EF38C9BB},renderingId={C24FDF95-5E41-50A9-BD28-9279D40E259B},id={047663DB-23C2-523C-832E-0E95FBB244B8})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{047663DB-23C2-523C-832E-0E95FBB244B8}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"C24FDF955E4150A9BD289279D40E259B\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Tracking\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Tracking", + "id": "r_7F765FCB3B1058FD8AA7B346EF38C9BB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7f765fcb-3b10-58fd-8aa7-b346ef38c9bb", + "componentName": "Styleguide-Tracking", + "dataSource": "{047663DB-23C2-523C-832E-0E95FBB244B8}", + "params": {}, + "fields": { + "heading": { + "value": "Tracking", + "editable": "{\"contextItem\":{\"id\":\"047663db-23c2-523c-832e-0e95fbb244b8\",\"version\":1,\"language\":\"en\",\"revision\":\"110ff663bb8d44edb7e2d4f248a60870\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{047663DB-23C2-523C-832E-0E95FBB244B8}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Tracking" + }, + "description": { + "value": "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

", + "editable": "{\"contextItem\":{\"id\":\"047663db-23c2-523c-832e-0e95fbb244b8\",\"version\":1,\"language\":\"en\",\"revision\":\"110ff663bb8d44edb7e2d4f248a60870\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{047663DB-23C2-523C-832E-0E95FBB244B8}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Tracking", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"1966771a-0941-5b93-9ac3-42dabe031ea1\",\"version\":1,\"language\":\"en\",\"revision\":\"2dd81335d4b24ee083cd4a94c449f3fa\"},\"renderingId\":\"a7bfc343-f487-5215-9348-8fbb0d209fa6\",\"renderingInstanceId\":\"{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={1966771A-0941-5B93-9AC3-42DABE031EA1})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={66AF8F03-0B52-5425-A6AF-6FB54F2D64D9},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={1966771A-0941-5B93-9AC3-42DABE031EA1})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={66AF8F03-0B52-5425-A6AF-6FB54F2D64D9},renderingId={A7BFC343-F487-5215-9348-8FBB0D209FA6},id={1966771A-0941-5B93-9AC3-42DABE031EA1})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1966771A-0941-5B93-9AC3-42DABE031EA1}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"A7BFC343F487521593488FBB0D209FA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_66AF8F030B525425A6AF6FB54F2D64D9", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "66af8f03-0b52-5425-a6af-6fb54f2d64d9", + "componentName": "Styleguide-Section", + "dataSource": "{1966771A-0941-5B93-9AC3-42DABE031EA1}", + "params": {}, + "fields": { + "heading": { + "value": "Multilingual Patterns", + "editable": "{\"contextItem\":{\"id\":\"1966771a-0941-5b93-9ac3-42dabe031ea1\",\"version\":1,\"language\":\"en\",\"revision\":\"2dd81335d4b24ee083cd4a94c449f3fa\"},\"fieldId\":\"ac4286bb-8f0f-5ba1-b2d1-fca768658290\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1966771A-0941-5B93-9AC3-42DABE031EA1}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Multilingual Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"8f7bef75-28a5-54f0-b7c4-998b51b67c75\",\"version\":1,\"language\":\"en\",\"revision\":\"60748843912c4eb5a66c94e9e275e52b\"},\"placeholderKey\":\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}-0\",\"placeholderMetadataKeys\":[\"/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section\",\"jss-styleguide-section\"],\"editable\":true,\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{8F7BEF75-28A5-54F0-B7C4-998B51B67C75}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"F7F8ABE4DFBD526CBEC96CFF8D8F8C2E\",\"76FB99EF095B513DAA99CF0417582C61\",\"CE8CE858ED84594FAADC25691D673893\",\"5D5AA98C7F85542BBA008A760B25C350\",\"DF215418C334545FABA807C2DA2957DB\",\"308B7BEC89225FED848B22056304212B\",\"7FC8F8F3C129520CA9FC43B9F834F816\",\"DE5D5DE25D715629AB2E18A4BF4B1D6D\",\"D94F5D1FB1BB57C8A752DD6A3D283878\",\"84B833652AF85A79B719E5381BF8CDEF\",\"A9C5FAF6C0EF5E32A0E1A056393786AC\",\"D92203380E605AB6B894BABC4F9DC5B9\",\"B592151D5AD0513D8037440F550D9D4C\",\"1B482CC5C9015A46BC42444C31A3AC60\",\"4D93967180FB50D0AAF5FCF281CE5F4F\",\"A9E1268B25DA5D1EA5C771B410D32E4D\",\"C24FDF955E4150A9BD289279D40E259B\",\"18A84B80BE55538F827E4E55970138A1\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__66AF8F03_0B52_5425_A6AF_6FB54F2D64D9__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"contextItem\":{\"id\":\"5217ffff-e020-5eeb-8d06-445a672fb480\",\"version\":1,\"language\":\"en\",\"revision\":\"dbae49bdeb0e415dbcdc2fec0cfff0fe\"},\"renderingId\":\"18a84b80-be55-538f-827e-4e55970138a1\",\"renderingInstanceId\":\"{CF1B5D2B-C949-56E7-9594-66AFACEACA9D}\",\"editable\":true,\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|heading|description,id={5217FFFF-E020-5EEB-8D06-445A672FB480})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={CF1B5D2B-C949-56E7-9594-66AFACEACA9D},renderingId={18A84B80-BE55-538F-827E-4E55970138A1},id={5217FFFF-E020-5EEB-8D06-445A672FB480})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={CF1B5D2B-C949-56E7-9594-66AFACEACA9D},renderingId={18A84B80-BE55-538F-827E-4E55970138A1},id={5217FFFF-E020-5EEB-8D06-445A672FB480})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5217FFFF-E020-5EEB-8D06-445A672FB480}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"18A84B80BE55538F827E4E55970138A1\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Multilingual\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Multilingual", + "id": "r_CF1B5D2BC94956E7959466AFACEACA9D", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "cf1b5d2b-c949-56e7-9594-66afaceaca9d", + "componentName": "Styleguide-Multilingual", + "dataSource": "{5217FFFF-E020-5EEB-8D06-445A672FB480}", + "params": {}, + "fields": { + "sample": { + "value": "This text can be translated in en.yml", + "editable": "{\"contextItem\":{\"id\":\"5217ffff-e020-5eeb-8d06-445a672fb480\",\"version\":1,\"language\":\"en\",\"revision\":\"dbae49bdeb0e415dbcdc2fec0cfff0fe\"},\"fieldId\":\"06b849dc-fb49-5dc7-8cd4-9cd9ae66ab21\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5217FFFF-E020-5EEB-8D06-445A672FB480}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"This field has a translated value\",\"expandedDisplayName\":null}This text can be translated in en.yml" + }, + "heading": { + "value": "Translation Patterns", + "editable": "{\"contextItem\":{\"id\":\"5217ffff-e020-5eeb-8d06-445a672fb480\",\"version\":1,\"language\":\"en\",\"revision\":\"dbae49bdeb0e415dbcdc2fec0cfff0fe\"},\"fieldId\":\"78202d1e-6710-58c1-bb4c-47e670537661\",\"fieldType\":\"Single-Line Text\",\"fieldWebEditParameters\":{\"prevent-line-break\":\"true\"},\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5217FFFF-E020-5EEB-8D06-445A672FB480}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Translation Patterns" + }, + "description": { + "value": "", + "editable": "{\"contextItem\":{\"id\":\"5217ffff-e020-5eeb-8d06-445a672fb480\",\"version\":1,\"language\":\"en\",\"revision\":\"dbae49bdeb0e415dbcdc2fec0cfff0fe\"},\"fieldId\":\"fddee192-073d-566f-9b28-1478933c693d\",\"fieldType\":\"Rich Text\",\"fieldWebEditParameters\":{},\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5217FFFF-E020-5EEB-8D06-445A672FB480}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Multilingual", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-layout", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "Main", + "class": "scpm" + } + } + ] + } + }, + "devices": [ + { + "id": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "885b8314-7d8c-4cbb-8000-01421ea8f406", + "instanceId": "43222d12-08c9-453b-ae96-d406ebb95126", + "placeholderKey": "main", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "ce4adcfb-7990-4980-83fb-a00c1e3673db", + "instanceId": "cf044ad9-0332-407a-abde-587214a2c808", + "placeholderKey": "/main/centercolumn", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "b343725a-3a93-446e-a9c8-3a2cbd3db489", + "placeholderKey": "/main/centercolumn/content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + }, + { + "id": "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + "placeholderKey": "content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + } + ] + } +} diff --git a/tests/data/json/edit.json b/tests/data/json/edit.json new file mode 100644 index 0000000..269ee4c --- /dev/null +++ b/tests/data/json/edit.json @@ -0,0 +1,1837 @@ +{ + "sitecore": { + "context": { + "pageEditing": true, + "user": { + "domain": "sitecore", + "name": "admin" + }, + "site": { "name": "sample" }, + "pageState": "edit", + "language": "en" + }, + "route": { + "name": "styleguide", + "displayName": "styleguide", + "fields": { + "pageTitle": { + "value": "Styleguide | Sitecore JSS", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Page Title\",\"expandedDisplayName\":null}Styleguide | Sitecore JSS" + } + }, + "databaseName": "master", + "deviceId": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "itemId": "616e2daa-bb71-5117-82b1-b360ef600213", + "itemLanguage": "en", + "itemVersion": 1, + "layoutId": "a194a0d1-1d5e-58bc-965e-e41753b376b2", + "templateId": "94fa898e-9fe1-5807-b743-52c97e3e8969", + "templateName": "App Route", + "placeholders": { + "jss-main": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "jss_main", + "key": "jss-main", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_E02DDB9BA0625E50924A1940D7E053CE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "e02ddb9b-a062-5e50-924a-1940d7e053ce", + "componentName": "ContentBlock", + "dataSource": "{585596CA-7903-500B-8DF2-0357DD6E3FAC}", + "fields": { + "heading": { + "value": "JSS Styleguide", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}JSS Styleguide" + }, + "content": { + "value": "

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"4E3C94B3A9D25478B7548D87283D8AA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout", + "id": "r_34A6553C81DE5CD3989E853F6CB6DF8C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "34a6553c-81de-5cd3-989e-853f6cb6df8c", + "componentName": "Styleguide-Layout", + "dataSource": "", + "placeholders": { + "jss-styleguide-layout": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"2BE039323CBF5024A9D329E8D0339010\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={906BFD66-8F75-5C22-9284-814C54673649})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={906BFD66-8F75-5C22-9284-814C54673649})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B7C779DA-2B75-586C-B40D-081FCB864256},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={906BFD66-8F75-5C22-9284-814C54673649})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{906BFD66-8F75-5C22-9284-814C54673649}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2BE039323CBF5024A9D329E8D0339010\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_B7C779DA2B75586CB40D081FCB864256", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "b7c779da-2b75-586c-b40d-081fcb864256", + "componentName": "Styleguide-Section", + "dataSource": "{906BFD66-8F75-5C22-9284-814C54673649}", + "fields": { + "heading": { + "value": "Content Data", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{906BFD66-8F75-5C22-9284-814C54673649}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Content Data" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"9D2D9D9CE9A25441926DA6812846C38D\",\"C9269300C2895648B572E6746C801C64\",\"367D6C1DB9985FA1B63DF48D03D2F8BD\",\"2F1D041635E1599D8FA6071EA1EEF2F7\",\"8E7F99EBE0FF5B9F9FD8A9FE0AF250ED\",\"3AAD655F10825CF8B7CEC1CFD15907D0\",\"979DF740D0BE579A9A2A5CEA7F3B2644\",\"7E20A4D5E8A258BB9FC116E9D00910D4\",\"12E44B0639515D3587A7695822E7FF82\",\"F42735C8C3815A6890FDD5B404A0E086\",\"F8D933858F385685A7F7E3AE290E647D\",\"9E93AE70E62657C3B34719C0671EC102\",\"8D929C7673E35409950B9A1A1E2CD98C\",\"562D5AD81E765A2B8161477852AD7454\",\"845CD18D5F815B338A2114B518425E16\",\"86EE9BB735085B85A8309ABFE16346D3\",\"2131857F0CA45425A6A0221BA1F3C12D\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__B7C779DA_2B75_586C_B40D_081FCB864256__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{B7C779DA-2B75-586C-B40D-081FCB864256}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={0B92F19B-5984-56A7-9FFA-D2E44545F077})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={63B0C99E-DAC7-5670-9D66-C26A78000EAE},renderingId={030E0BA8-B5B8-544F-B424-4BB1D83CDA2E},id={0B92F19B-5984-56A7-9FFA-D2E44545F077})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={63B0C99E-DAC7-5670-9D66-C26A78000EAE},renderingId={030E0BA8-B5B8-544F-B424-4BB1D83CDA2E},id={0B92F19B-5984-56A7-9FFA-D2E44545F077})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0B92F19B-5984-56A7-9FFA-D2E44545F077}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Text\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Text", + "id": "r_63B0C99EDAC756709D66C26A78000EAE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "63b0c99e-dac7-5670-9d66-c26a78000eae", + "componentName": "Styleguide-FieldUsage-Text", + "dataSource": "{0B92F19B-5984-56A7-9FFA-D2E44545F077}", + "fields": { + "sample": { + "value": "This is a sample text field. HTML is encoded. In Sitecore, editors will see a .", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0B92F19B-5984-56A7-9FFA-D2E44545F077}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}This is a sample text field. <mark>HTML is encoded.</mark> In Sitecore, editors will see a <input type="text">." + }, + "sample2": { + "value": "This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false.", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0B92F19B-5984-56A7-9FFA-D2E44545F077}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}This is another sample text field using rendering options. <mark>HTML supported with encode=false.</mark> Cannot edit because editable=false." + }, + "heading": { + "value": "Single-Line Text", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0B92F19B-5984-56A7-9FFA-D2E44545F077}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Single-Line Text" + }, + "description": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0B92F19B-5984-56A7-9FFA-D2E44545F077}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Text", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={F466F981-FD32-56BC-BA32-D752DC1DC730})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={F1EA3BB5-1175-5055-AB11-9C48BF69427A},renderingId={030E0BA8-B5B8-544F-B424-4BB1D83CDA2E},id={F466F981-FD32-56BC-BA32-D752DC1DC730})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={F1EA3BB5-1175-5055-AB11-9C48BF69427A},renderingId={030E0BA8-B5B8-544F-B424-4BB1D83CDA2E},id={F466F981-FD32-56BC-BA32-D752DC1DC730})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F466F981-FD32-56BC-BA32-D752DC1DC730}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Text\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Text", + "id": "r_F1EA3BB511755055AB119C48BF69427A", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "f1ea3bb5-1175-5055-ab11-9c48bf69427a", + "componentName": "Styleguide-FieldUsage-Text", + "dataSource": "{F466F981-FD32-56BC-BA32-D752DC1DC730}", + "fields": { + "sample": { + "value": "This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea.", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F466F981-FD32-56BC-BA32-D752DC1DC730}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}This is a sample multi-line text field. <mark>HTML is encoded.</mark> In Sitecore, editors will see a textarea." + }, + "sample2": { + "value": "This is another sample multi-line text field using rendering options. HTML supported with encode=false.", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F466F981-FD32-56BC-BA32-D752DC1DC730}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}This is another sample multi-line text field using rendering options. <mark>HTML supported with encode=false.</mark>" + }, + "heading": { + "value": "Multi-Line Text", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F466F981-FD32-56BC-BA32-D752DC1DC730}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Multi-Line Text" + }, + "description": { + "value": "Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text.", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F466F981-FD32-56BC-BA32-D752DC1DC730}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text." + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Text", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|sample2|heading|description,id={D017AC0B-473C-5FC4-999C-7E68234B9C5B})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={69CEBC00-446B-5141-AD1E-450B8D6EE0AD},renderingId={9D2D9D9C-E9A2-5441-926D-A6812846C38D},id={D017AC0B-473C-5FC4-999C-7E68234B9C5B})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={69CEBC00-446B-5141-AD1E-450B8D6EE0AD},renderingId={9D2D9D9C-E9A2-5441-926D-A6812846C38D},id={D017AC0B-473C-5FC4-999C-7E68234B9C5B})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D017AC0B-473C-5FC4-999C-7E68234B9C5B}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"9D2D9D9CE9A25441926DA6812846C38D\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-RichText\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-RichText", + "id": "r_69CEBC00446B5141AD1E450B8D6EE0AD", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "69cebc00-446b-5141-ad1e-450b8d6ee0ad", + "componentName": "Styleguide-FieldUsage-RichText", + "dataSource": "{D017AC0B-473C-5FC4-999C-7E68234B9C5B}", + "fields": { + "sample": { + "value": "

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D017AC0B-473C-5FC4-999C-7E68234B9C5B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

" + }, + "sample2": { + "value": "

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D017AC0B-473C-5FC4-999C-7E68234B9C5B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"Customize Name Shown in Sitecore\",\"expandedDisplayName\":null}

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n
" + }, + "heading": { + "value": "Rich Text", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D017AC0B-473C-5FC4-999C-7E68234B9C5B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Rich Text" + }, + "description": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D017AC0B-473C-5FC4-999C-7E68234B9C5B}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-RichText", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample1|sample2|heading|description,id={0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={5630C0E6-0430-5F6A-AF9E-2D09D600A386},renderingId={C9269300-C289-5648-B572-E6746C801C64},id={0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={5630C0E6-0430-5F6A-AF9E-2D09D600A386},renderingId={C9269300-C289-5648-B572-E6746C801C64},id={0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"C9269300C2895648B572E6746C801C64\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Image\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Image", + "id": "r_5630C0E604305F6AAF9E2D09D600A386", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "5630c0e6-0430-5f6a-af9e-2d09d600a386", + "componentName": "Styleguide-FieldUsage-Image", + "dataSource": "{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}", + "fields": { + "sample1": { + "value": { + "src": "/sitecore/shell/-/media/sample/data/media/img/sc_logo.png", + "alt": "Sitecore Logo" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample1\",\"expandedDisplayName\":null}\"Sitecore" + }, + "sample2": { + "value": { + "src": "/sitecore/shell/-/media/sample/data/media/img/jss_logo.png", + "alt": "Sitecore JSS Logo" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:chooseimage\\\"})\",\"header\":\"Choose Image\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2.png\",\"disabledIcon\":\"/temp/photo_landscape2_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Choose an image.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editimage\\\"})\",\"header\":\"Properties\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_edit.png\",\"disabledIcon\":\"/temp/photo_landscape2_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Modify image appearance.\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearimage\\\"})\",\"header\":\"Clear\",\"icon\":\"/sitecore/shell/themes/standard/custom/16x16/photo_landscape2_delete.png\",\"disabledIcon\":\"/temp/photo_landscape2_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove the image.\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample2\",\"expandedDisplayName\":null}\"Sitecore" + }, + "heading": { + "value": "Image", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Image" + }, + "description": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{0BFCD63A-5D5C-5A3F-AF5A-0E5F5F837556}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Image", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=file|heading|description,id={F87A579E-985B-5DEB-A809-C413D5A562D2})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={BAD43EF7-8940-504D-A09B-976C17A9A30C},renderingId={367D6C1D-B998-5FA1-B63D-F48D03D2F8BD},id={F87A579E-985B-5DEB-A809-C413D5A562D2})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={BAD43EF7-8940-504D-A09B-976C17A9A30C},renderingId={367D6C1D-B998-5FA1-B63D-F48D03D2F8BD},id={F87A579E-985B-5DEB-A809-C413D5A562D2})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F87A579E-985B-5DEB-A809-C413D5A562D2}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"367D6C1DB9985FA1B63DF48D03D2F8BD\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-File\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-File", + "id": "r_BAD43EF78940504DA09B976C17A9A30C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "bad43ef7-8940-504d-a09b-976c17a9a30c", + "componentName": "Styleguide-FieldUsage-File", + "dataSource": "{F87A579E-985B-5DEB-A809-C413D5A562D2}", + "fields": { + "file": { + "value": { + "src": "/sitecore/shell/-/media/sample/data/media/files/jss.pdf", + "name": "jss", + "displayName": "jss", + "title": "Example File", + "keywords": "", + "description": "This data will be added to the Sitecore Media Library on import", + "extension": "pdf", + "mimeType": "application/pdf", + "size": "156897" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F87A579E-985B-5DEB-A809-C413D5A562D2}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"file\",\"expandedDisplayName\":null}" + }, + "heading": { + "value": "File", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F87A579E-985B-5DEB-A809-C413D5A562D2}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}File" + }, + "description": { + "value": "Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{F87A579E-985B-5DEB-A809-C413D5A562D2}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-File", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|heading|description,id={5E722116-2210-50D6-B420-77D140B60992})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={FF90D4BD-E50D-5BBF-9213-D25968C9AE75},renderingId={2F1D0416-35E1-599D-8FA6-071EA1EEF2F7},id={5E722116-2210-50D6-B420-77D140B60992})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={FF90D4BD-E50D-5BBF-9213-D25968C9AE75},renderingId={2F1D0416-35E1-599D-8FA6-071EA1EEF2F7},id={5E722116-2210-50D6-B420-77D140B60992})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5E722116-2210-50D6-B420-77D140B60992}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2F1D041635E1599D8FA6071EA1EEF2F7\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Number\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Number", + "id": "r_FF90D4BDE50D5BBF9213D25968C9AE75", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "ff90d4bd-e50d-5bbf-9213-d25968c9ae75", + "componentName": "Styleguide-FieldUsage-Number", + "dataSource": "{5E722116-2210-50D6-B420-77D140B60992}", + "fields": { + "sample": { + "value": "1.21", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editnumber\\\"})\",\"header\":\"Edit number\",\"icon\":\"/temp/iconcache/wordprocessing/16x16/word_count.png\",\"disabledIcon\":\"/temp/word_count_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit number\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5E722116-2210-50D6-B420-77D140B60992}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"sample\",\"expandedDisplayName\":null}1.21" + }, + "heading": { + "value": "Number", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5E722116-2210-50D6-B420-77D140B60992}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Number" + }, + "description": { + "value": "Number tells Sitecore to use a number entry for editing.", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5E722116-2210-50D6-B420-77D140B60992}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Number tells Sitecore to use a number entry for editing." + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Number", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=checkbox|checkbox2|heading|description,id={68958360-3D48-58BA-95D1-36374993DADB})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={B5C1C74A-A81D-59B2-85D8-09BC109B1F70},renderingId={8E7F99EB-E0FF-5B9F-9FD8-A9FE0AF250ED},id={68958360-3D48-58BA-95D1-36374993DADB})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={B5C1C74A-A81D-59B2-85D8-09BC109B1F70},renderingId={8E7F99EB-E0FF-5B9F-9FD8-A9FE0AF250ED},id={68958360-3D48-58BA-95D1-36374993DADB})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{68958360-3D48-58BA-95D1-36374993DADB}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"8E7F99EBE0FF5B9F9FD8A9FE0AF250ED\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Checkbox\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Checkbox", + "id": "r_B5C1C74AA81D59B285D809BC109B1F70", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "b5c1c74a-a81d-59b2-85d8-09bc109b1f70", + "componentName": "Styleguide-FieldUsage-Checkbox", + "dataSource": "{68958360-3D48-58BA-95D1-36374993DADB}", + "fields": { + "checkbox": { + "value": true, + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{68958360-3D48-58BA-95D1-36374993DADB}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"checkbox\",\"expandedDisplayName\":null}1" + }, + "checkbox2": { + "value": false, + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{68958360-3D48-58BA-95D1-36374993DADB}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"checkbox2\",\"expandedDisplayName\":null}0" + }, + "heading": { + "value": "Checkbox", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{68958360-3D48-58BA-95D1-36374993DADB}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Checkbox" + }, + "description": { + "value": "Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{68958360-3D48-58BA-95D1-36374993DADB}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Checkbox", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=date|dateTime|heading|description,id={C7BD1719-EF67-567C-8B16-D6755597482E})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={F166A7D6-9EC8-5C53-B825-33405DB7F575},renderingId={3AAD655F-1082-5CF8-B7CE-C1CFD15907D0},id={C7BD1719-EF67-567C-8B16-D6755597482E})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={F166A7D6-9EC8-5C53-B825-33405DB7F575},renderingId={3AAD655F-1082-5CF8-B7CE-C1CFD15907D0},id={C7BD1719-EF67-567C-8B16-D6755597482E})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C7BD1719-EF67-567C-8B16-D6755597482E}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"3AAD655F10825CF8B7CEC1CFD15907D0\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Date\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Date", + "id": "r_F166A7D69EC85C53B82533405DB7F575", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "f166a7d6-9ec8-5c53-b825-33405db7f575", + "componentName": "Styleguide-FieldUsage-Date", + "dataSource": "{C7BD1719-EF67-567C-8B16-D6755597482E}", + "fields": { + "date": { + "value": "2012-05-04T00:00:00Z", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editdate\\\"})\",\"header\":\"Show calendar\",\"icon\":\"/temp/iconcache/business/16x16/calendar.png\",\"disabledIcon\":\"/temp/calendar_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Shows the calendar\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C7BD1719-EF67-567C-8B16-D6755597482E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"date\",\"expandedDisplayName\":null}5/4/2012 2:00:00 AM" + }, + "dateTime": { + "value": "2018-03-14T15:00:00Z", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editdate\\\"})\",\"header\":\"Show calendar\",\"icon\":\"/temp/iconcache/business/16x16/calendar.png\",\"disabledIcon\":\"/temp/calendar_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Shows the calendar\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C7BD1719-EF67-567C-8B16-D6755597482E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"dateTime\",\"expandedDisplayName\":null}3/14/2018 4:00:00 PM" + }, + "heading": { + "value": "Date", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C7BD1719-EF67-567C-8B16-D6755597482E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Date" + }, + "description": { + "value": "

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C7BD1719-EF67-567C-8B16-D6755597482E}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Date", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=externalLink|internalLink|emailLink|paramsLink|heading|description,id={A33B789F-794E-5E2B-B8A0-8A2112DD848D})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={56A9562A-6813-579B-8ED2-FDDAB1BFD3D2},renderingId={979DF740-D0BE-579A-9A2A-5CEA7F3B2644},id={A33B789F-794E-5E2B-B8A0-8A2112DD848D})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={56A9562A-6813-579B-8ED2-FDDAB1BFD3D2},renderingId={979DF740-D0BE-579A-9A2A-5CEA7F3B2644},id={A33B789F-794E-5E2B-B8A0-8A2112DD848D})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"979DF740D0BE579A9A2A5CEA7F3B2644\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Link\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Link", + "id": "r_56A9562A6813579B8ED2FDDAB1BFD3D2", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "56a9562a-6813-579b-8ed2-fddab1bfd3d2", + "componentName": "Styleguide-FieldUsage-Link", + "dataSource": "{A33B789F-794E-5E2B-B8A0-8A2112DD848D}", + "fields": { + "externalLink": { + "value": { + "href": "https://www.sitecore.com", + "text": "Link to Sitecore", + "url": "https://www.sitecore.com", + "linktype": "external" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"externalLink\",\"expandedDisplayName\":null}Link to Sitecore", + "editableFirstPart": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"externalLink\",\"expandedDisplayName\":null}Link to Sitecore", + "editableLastPart": "" + }, + "internalLink": { + "value": { + "href": "/", + "linktype": "internal", + "id": "{4E8DA8B5-A330-5BF2-A16F-A95249BEA771}" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"internalLink\",\"expandedDisplayName\":null}home", + "editableFirstPart": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"internalLink\",\"expandedDisplayName\":null}home", + "editableLastPart": "" + }, + "emailLink": { + "value": { + "href": "mailto:foo@bar.com", + "text": "Send an Email", + "url": "mailto:foo@bar.com", + "linktype": "mailto" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"emailLink\",\"expandedDisplayName\":null}Send an Email", + "editableFirstPart": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"emailLink\",\"expandedDisplayName\":null}Send an Email", + "editableLastPart": "" + }, + "paramsLink": { + "value": { + "href": "https://dev.sitecore.net", + "target": "_blank", + "text": "Sitecore Dev Site", + "title": " title attribute", + "url": "https://dev.sitecore.net", + "class": "font-weight-bold", + "linktype": "external" + }, + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"paramsLink\",\"expandedDisplayName\":null} title attribute\" href=\"https://dev.sitecore.net\" target=\"_blank\">Sitecore Dev Site", + "editableFirstPart": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editlink\\\"})\",\"header\":\"Edit link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_edit.png\",\"disabledIcon\":\"/temp/link_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edits the link destination and appearance\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:clearlink\\\"})\",\"header\":\"Clear Link\",\"icon\":\"/temp/iconcache/networkv2/16x16/link_delete.png\",\"disabledIcon\":\"/temp/link_delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Clears The Link\",\"type\":\"\"},{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:followlink\\\"})\",\"header\":\"Follow Link\",\"icon\":\"/temp/iconcache/applications/16x16/arrow_right_blue.png\",\"disabledIcon\":\"/temp/arrow_right_blue_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Follow Link\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"paramsLink\",\"expandedDisplayName\":null} title attribute\" href=\"https://dev.sitecore.net\" target=\"_blank\">Sitecore Dev Site", + "editableLastPart": "" + }, + "heading": { + "value": "General Link", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}General Link" + }, + "description": { + "value": "

A General Link is a field that represents an <a> tag.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A33B789F-794E-5E2B-B8A0-8A2112DD848D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

A General Link is a field that represents an <a> tag.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Link", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sharedItemLink|localItemLink|heading|description,id={A6E2D0F8-30F9-5136-B940-7C144B38C97D})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A44AD1F8-0582-5248-9DF9-52429193A68B},renderingId={7E20A4D5-E8A2-58BB-9FC1-16E9D00910D4},id={A6E2D0F8-30F9-5136-B940-7C144B38C97D})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A44AD1F8-0582-5248-9DF9-52429193A68B},renderingId={7E20A4D5-E8A2-58BB-9FC1-16E9D00910D4},id={A6E2D0F8-30F9-5136-B940-7C144B38C97D})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A6E2D0F8-30F9-5136-B940-7C144B38C97D}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"7E20A4D5E8A258BB9FC116E9D00910D4\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-ItemLink\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-ItemLink", + "id": "r_A44AD1F8058252489DF952429193A68B", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a44ad1f8-0582-5248-9df9-52429193a68b", + "componentName": "Styleguide-FieldUsage-ItemLink", + "dataSource": "{A6E2D0F8-30F9-5136-B940-7C144B38C97D}", + "fields": { + "sharedItemLink": { + "id": "6eaa2b9e-188c-5eb5-af05-26365b328777", + "url": "/Content/Styleguide/ItemLinkField/Item1", + "fields": { + "textField": { + "value": "ItemLink Demo (Shared) Item 1 Text Field", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6EAA2B9E-188C-5EB5-AF05-26365B328777}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ItemLink Demo (Shared) Item 1 Text Field" + } + } + }, + "localItemLink": { + "id": "eafbdcfb-3eaf-58a8-a18f-8ce8f0d8c9d6", + "url": "/styleguide/Page-Components/styleguide-jss-styleguide-section-B73482E131E5A083D77A50554BC74A4758E29636DF6824F6E2F272EE778C28A095/styleguide-jss-styleguide-section-B75151F05CFDC4CAFFE44E5BAED9D59BEA82565EC11CE75B7DEF3634495EC1DAB7", + "fields": { + "textField": { + "value": "Referenced item textField", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{EAFBDCFB-3EAF-58A8-A18F-8CE8F0D8C9D6}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Referenced item textField" + } + } + }, + "heading": { + "value": "Item Link", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A6E2D0F8-30F9-5136-B940-7C144B38C97D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Item Link" + }, + "description": { + "value": "

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{A6E2D0F8-30F9-5136-B940-7C144B38C97D}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-ItemLink", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sharedContentList|localContentList|heading|description,id={E38C52CB-2C40-5779-847C-D0E129889377})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={2F609D40-8AD9-540E-901E-23AA2600F3EB},renderingId={12E44B06-3951-5D35-87A7-695822E7FF82},id={E38C52CB-2C40-5779-847C-D0E129889377})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={2F609D40-8AD9-540E-901E-23AA2600F3EB},renderingId={12E44B06-3951-5D35-87A7-695822E7FF82},id={E38C52CB-2C40-5779-847C-D0E129889377})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E38C52CB-2C40-5779-847C-D0E129889377}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"12E44B0639515D3587A7695822E7FF82\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-ContentList\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-ContentList", + "id": "r_2F609D408AD9540E901E23AA2600F3EB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "2f609d40-8ad9-540e-901e-23aa2600f3eb", + "componentName": "Styleguide-FieldUsage-ContentList", + "dataSource": "{E38C52CB-2C40-5779-847C-D0E129889377}", + "fields": { + "sharedContentList": [ + { + "id": "5a620ce0-5ba4-5238-b3bf-d7364ff385f6", + "fields": { + "textField": { + "value": "ContentList Demo (Shared) Item 1 Text Field", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{5A620CE0-5BA4-5238-B3BF-D7364FF385F6}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ContentList Demo (Shared) Item 1 Text Field" + } + } + }, + { + "id": "d40ee431-1fcf-5bba-903b-d90aac57f21a", + "fields": { + "textField": { + "value": "ContentList Demo (Shared) Item 2 Text Field", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D40EE431-1FCF-5BBA-903B-D90AAC57F21A}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}ContentList Demo (Shared) Item 2 Text Field" + } + } + } + ], + "localContentList": [ + { + "id": "566055f2-0c7d-5284-a0c3-d0cea1b18f58", + "fields": { + "textField": { + "value": "Hello World Item 1", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{566055F2-0C7D-5284-A0C3-D0CEA1B18F58}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Hello World Item 1" + } + } + }, + { + "id": "6593859b-ab0d-531d-be28-a452d162d7f1", + "fields": { + "textField": { + "value": "Hello World Item 2", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6593859B-AB0D-531D-BE28-A452D162D7F1}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"textField\",\"expandedDisplayName\":null}Hello World Item 2" + } + } + } + ], + "heading": { + "value": "Content List", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E38C52CB-2C40-5779-847C-D0E129889377}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Content List" + }, + "description": { + "value": "

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{E38C52CB-2C40-5779-847C-D0E129889377}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-ContentList", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=customIntField|heading|description,id={B5981AFB-DEA8-5424-9466-AC93B359BB65})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={352ED63D-796A-5523-89F5-9A991DDA4A8F},renderingId={F42735C8-C381-5A68-90FD-D5B404A0E086},id={B5981AFB-DEA8-5424-9466-AC93B359BB65})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={352ED63D-796A-5523-89F5-9A991DDA4A8F},renderingId={F42735C8-C381-5A68-90FD-D5B404A0E086},id={B5981AFB-DEA8-5424-9466-AC93B359BB65})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B5981AFB-DEA8-5424-9466-AC93B359BB65}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"F42735C8C3815A6890FDD5B404A0E086\",\"editable\":\"true\"},\"displayName\":\"Styleguide-FieldUsage-Custom\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-FieldUsage-Custom", + "id": "r_352ED63D796A552389F59A991DDA4A8F", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "352ed63d-796a-5523-89f5-9a991dda4a8f", + "componentName": "Styleguide-FieldUsage-Custom", + "dataSource": "{B5981AFB-DEA8-5424-9466-AC93B359BB65}", + "fields": { + "customIntField": { + "value": "31337", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:editinteger\\\"})\",\"header\":\"Edit integer\",\"icon\":\"/temp/iconcache/wordprocessing/16x16/word_count.png\",\"disabledIcon\":\"/temp/word_count_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit integer\",\"type\":\"\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B5981AFB-DEA8-5424-9466-AC93B359BB65}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"customIntField\",\"expandedDisplayName\":null}31337" + }, + "heading": { + "value": "Custom Fields", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B5981AFB-DEA8-5424-9466-AC93B359BB65}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Custom Fields" + }, + "description": { + "value": "

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B5981AFB-DEA8-5424-9466-AC93B359BB65}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n
" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-FieldUsage-Custom", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={4866E790-8835-537C-BCF2-0D0479CFCD50})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7DE41A1A-24E4-5963-8206-3BB0B7D9DD69},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={4866E790-8835-537C-BCF2-0D0479CFCD50})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7DE41A1A-24E4-5963-8206-3BB0B7D9DD69},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={4866E790-8835-537C-BCF2-0D0479CFCD50})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4866E790-8835-537C-BCF2-0D0479CFCD50}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2BE039323CBF5024A9D329E8D0339010\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_7DE41A1A24E4596382063BB0B7D9DD69", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7de41a1a-24e4-5963-8206-3bb0b7d9dd69", + "componentName": "Styleguide-Section", + "dataSource": "{4866E790-8835-537C-BCF2-0D0479CFCD50}", + "fields": { + "heading": { + "value": "Layout Patterns", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{4866E790-8835-537C-BCF2-0D0479CFCD50}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Layout Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"9D2D9D9CE9A25441926DA6812846C38D\",\"C9269300C2895648B572E6746C801C64\",\"367D6C1DB9985FA1B63DF48D03D2F8BD\",\"2F1D041635E1599D8FA6071EA1EEF2F7\",\"8E7F99EBE0FF5B9F9FD8A9FE0AF250ED\",\"3AAD655F10825CF8B7CEC1CFD15907D0\",\"979DF740D0BE579A9A2A5CEA7F3B2644\",\"7E20A4D5E8A258BB9FC116E9D00910D4\",\"12E44B0639515D3587A7695822E7FF82\",\"F42735C8C3815A6890FDD5B404A0E086\",\"F8D933858F385685A7F7E3AE290E647D\",\"9E93AE70E62657C3B34719C0671EC102\",\"8D929C7673E35409950B9A1A1E2CD98C\",\"562D5AD81E765A2B8161477852AD7454\",\"845CD18D5F815B338A2114B518425E16\",\"86EE9BB735085B85A8309ABFE16346D3\",\"2131857F0CA45425A6A0221BA1F3C12D\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={256716C3-9F59-5442-AD0B-C14D0EEA19F6})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A},renderingId={F8D93385-8F38-5685-A7F7-E3AE290E647D},id={256716C3-9F59-5442-AD0B-C14D0EEA19F6})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A},renderingId={F8D93385-8F38-5685-A7F7-E3AE290E647D},id={256716C3-9F59-5442-AD0B-C14D0EEA19F6})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{256716C3-9F59-5442-AD0B-C14D0EEA19F6}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"F8D933858F385685A7F7E3AE290E647D\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Reuse\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Reuse", + "id": "r_3A5D9C50D8C15A128DA85D56C2A5A69A", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "3a5d9c50-d8c1-5a12-8da8-5d56c2a5a69a", + "componentName": "Styleguide-Layout-Reuse", + "dataSource": "{256716C3-9F59-5442-AD0B-C14D0EEA19F6}", + "fields": { + "heading": { + "value": "Reusing Content", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{256716C3-9F59-5442-AD0B-C14D0EEA19F6}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Reusing Content" + }, + "description": { + "value": "

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{256716C3-9F59-5442-AD0B-C14D0EEA19F6}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

" + } + }, + "placeholders": { + "jss-reuse-example": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\"],\"editable\":\"true\"},\"displayName\":\"jss-reuse-example\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0_jss_reuse_example__3A5D9C50_D8C1_5A12_8DA8_5D56C2A5A69A__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-reuse-example-{3A5D9C50-D8C1-5A12-8DA8-5D56C2A5A69A}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={AA328B8A-D6E1-5B37-8143-250D2E93D6B8},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={AA328B8A-D6E1-5B37-8143-250D2E93D6B8},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_AA328B8AD6E15B378143250D2E93D6B8", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "aa328b8a-d6e1-5b37-8143-250d2e93d6b8", + "componentName": "ContentBlock", + "dataSource": "{99719E9F-8CF4-5DF0-B0ED-CE6311457241}", + "fields": { + "heading": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={C4330D34-623C-556C-BF4C-97C93D40FB1E},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={C4330D34-623C-556C-BF4C-97C93D40FB1E},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={99719E9F-8CF4-5DF0-B0ED-CE6311457241})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_C4330D34623C556CBF4C97C93D40FB1E", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "c4330d34-623c-556c-bf4c-97c93d40fb1e", + "componentName": "ContentBlock", + "dataSource": "{99719E9F-8CF4-5DF0-B0ED-CE6311457241}", + "fields": { + "heading": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{99719E9F-8CF4-5DF0-B0ED-CE6311457241}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A42D8B1C-193D-5627-9130-F7F7F87617F1},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A42D8B1C-193D-5627-9130-F7F7F87617F1},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_A42D8B1C193D56279130F7F7F87617F1", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a42d8b1c-193d-5627-9130-f7f7f87617f1", + "componentName": "ContentBlock", + "dataSource": "{1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE}", + "fields": { + "heading": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1FC6E0D3-8413-5DC4-BB94-B2FBC8C768CE}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={0F4CB47A-979E-5139-B50B-A8E40C73C236},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={0F4CB47A-979E-5139-B50B-A8E40C73C236},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_0F4CB47A979E5139B50BA8E40C73C236", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "0f4cb47a-979e-5139-b50b-a8e40c73c236", + "componentName": "ContentBlock", + "dataSource": "{1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575}", + "fields": { + "heading": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}[No text in field]" + }, + "content": { + "value": "

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{1A0D7A75-D9E4-5CF1-9C78-5D960B7E6575}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-reuse-example", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Reuse", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={538E4831-F157-50BB-AC74-277FCAC9FDDB},renderingId={9E93AE70-E626-57C3-B347-19C0671EC102},id={C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={538E4831-F157-50BB-AC74-277FCAC9FDDB},renderingId={9E93AE70-E626-57C3-B347-19C0671EC102},id={C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"9E93AE70E62657C3B34719C0671EC102\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs", + "id": "r_538E4831F15750BBAC74277FCAC9FDDB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "538e4831-f157-50bb-ac74-277fcac9fddb", + "componentName": "Styleguide-Layout-Tabs", + "dataSource": "{C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F}", + "fields": { + "heading": { + "value": "Tabs", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Tabs" + }, + "description": { + "value": "

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{C1250FCB-D6FB-57DB-8FDB-08EF98A43F1F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

" + } + }, + "placeholders": { + "jss-tabs": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"E17C84BA85975BAC8C4EF36E75D1BF52\"],\"editable\":\"true\"},\"displayName\":\"Tabs\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__7DE41A1A_24E4_5963_8206_3BB0B7D9DD69__0_jss_tabs__538E4831_F157_50BB_AC74_277FCAC9FDDB__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{7DE41A1A-24E4-5963-8206-3BB0B7D9DD69}-0/jss-tabs-{538E4831-F157-50BB-AC74-277FCAC9FDDB}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={D4647DDC-6516-5251-B38D-04C203501D21})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7ECB2ED2-AC9B-58D1-8365-10CA74824AF7},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={D4647DDC-6516-5251-B38D-04C203501D21})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7ECB2ED2-AC9B-58D1-8365-10CA74824AF7},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={D4647DDC-6516-5251-B38D-04C203501D21})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4647DDC-6516-5251-B38D-04C203501D21}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"E17C84BA85975BAC8C4EF36E75D1BF52\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_7ECB2ED2AC9B58D1836510CA74824AF7", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7ecb2ed2-ac9b-58d1-8365-10ca74824af7", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{D4647DDC-6516-5251-B38D-04C203501D21}", + "fields": { + "title": { + "value": "Tab 1", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4647DDC-6516-5251-B38D-04C203501D21}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 1" + }, + "content": { + "value": "

Tab 1 contents!

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D4647DDC-6516-5251-B38D-04C203501D21}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 1 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={6F26BB8B-C905-528E-941E-FC617774F971})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={AFD64900-0A61-50EB-A674-A7A884E0D496},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={6F26BB8B-C905-528E-941E-FC617774F971})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={AFD64900-0A61-50EB-A674-A7A884E0D496},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={6F26BB8B-C905-528E-941E-FC617774F971})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6F26BB8B-C905-528E-941E-FC617774F971}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"E17C84BA85975BAC8C4EF36E75D1BF52\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_AFD649000A6150EBA674A7A884E0D496", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "afd64900-0a61-50eb-a674-a7a884e0d496", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{6F26BB8B-C905-528E-941E-FC617774F971}", + "fields": { + "title": { + "value": "Tab 2", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6F26BB8B-C905-528E-941E-FC617774F971}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 2" + }, + "content": { + "value": "

Tab 2 contents!

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{6F26BB8B-C905-528E-941E-FC617774F971}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 2 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=title|content,id={2C770B17-39F3-5010-88D1-9F8C94AA11A3})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={44C12983-3A84-5462-84C0-6CA1430050C8},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={2C770B17-39F3-5010-88D1-9F8C94AA11A3})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={44C12983-3A84-5462-84C0-6CA1430050C8},renderingId={E17C84BA-8597-5BAC-8C4E-F36E75D1BF52},id={2C770B17-39F3-5010-88D1-9F8C94AA11A3})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2C770B17-39F3-5010-88D1-9F8C94AA11A3}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"E17C84BA85975BAC8C4EF36E75D1BF52\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout-Tabs-Tab\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout-Tabs-Tab", + "id": "r_44C129833A84546284C06CA1430050C8", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "44c12983-3a84-5462-84c0-6ca1430050c8", + "componentName": "Styleguide-Layout-Tabs-Tab", + "dataSource": "{2C770B17-39F3-5010-88D1-9F8C94AA11A3}", + "fields": { + "title": { + "value": "Tab 3", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2C770B17-39F3-5010-88D1-9F8C94AA11A3}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"title\",\"expandedDisplayName\":null}Tab 3" + }, + "content": { + "value": "

Tab 3 contents!

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2C770B17-39F3-5010-88D1-9F8C94AA11A3}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"content\",\"expandedDisplayName\":null}

Tab 3 contents!

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs-Tab", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "Tabs", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout-Tabs", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={63039F89-721D-5425-A4D0-017215C9A44F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={2D806C25-DD46-51E3-93DE-63CF9035122C},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={63039F89-721D-5425-A4D0-017215C9A44F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={2D806C25-DD46-51E3-93DE-63CF9035122C},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={63039F89-721D-5425-A4D0-017215C9A44F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{63039F89-721D-5425-A4D0-017215C9A44F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2BE039323CBF5024A9D329E8D0339010\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_2D806C25DD4651E393DE63CF9035122C", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "2d806c25-dd46-51e3-93de-63cf9035122c", + "componentName": "Styleguide-Section", + "dataSource": "{63039F89-721D-5425-A4D0-017215C9A44F}", + "fields": { + "heading": { + "value": "Sitecore Patterns", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{63039F89-721D-5425-A4D0-017215C9A44F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Sitecore Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"9D2D9D9CE9A25441926DA6812846C38D\",\"C9269300C2895648B572E6746C801C64\",\"367D6C1DB9985FA1B63DF48D03D2F8BD\",\"2F1D041635E1599D8FA6071EA1EEF2F7\",\"8E7F99EBE0FF5B9F9FD8A9FE0AF250ED\",\"3AAD655F10825CF8B7CEC1CFD15907D0\",\"979DF740D0BE579A9A2A5CEA7F3B2644\",\"7E20A4D5E8A258BB9FC116E9D00910D4\",\"12E44B0639515D3587A7695822E7FF82\",\"F42735C8C3815A6890FDD5B404A0E086\",\"F8D933858F385685A7F7E3AE290E647D\",\"9E93AE70E62657C3B34719C0671EC102\",\"8D929C7673E35409950B9A1A1E2CD98C\",\"562D5AD81E765A2B8161477852AD7454\",\"845CD18D5F815B338A2114B518425E16\",\"86EE9BB735085B85A8309ABFE16346D3\",\"2131857F0CA45425A6A0221BA1F3C12D\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__2D806C25_DD46_51E3_93DE_63CF9035122C__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{2D806C25-DD46-51E3-93DE-63CF9035122C}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={32E87144-8DF8-5270-B5DE-5B940A2F97E0})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={471FA16A-BB82-5C42-9C95-E7EAB1E3BD30},renderingId={8D929C76-73E3-5409-950B-9A1A1E2CD98C},id={32E87144-8DF8-5270-B5DE-5B940A2F97E0})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={471FA16A-BB82-5C42-9C95-E7EAB1E3BD30},renderingId={8D929C76-73E3-5409-950B-9A1A1E2CD98C},id={32E87144-8DF8-5270-B5DE-5B940A2F97E0})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32E87144-8DF8-5270-B5DE-5B940A2F97E0}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"8D929C7673E35409950B9A1A1E2CD98C\",\"editable\":\"true\"},\"displayName\":\"Styleguide-SitecoreContext\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-SitecoreContext", + "id": "r_471FA16ABB825C429C95E7EAB1E3BD30", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "471fa16a-bb82-5c42-9c95-e7eab1e3bd30", + "componentName": "Styleguide-SitecoreContext", + "dataSource": "{32E87144-8DF8-5270-B5DE-5B940A2F97E0}", + "fields": { + "heading": { + "value": "Sitecore Context", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32E87144-8DF8-5270-B5DE-5B940A2F97E0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Sitecore Context" + }, + "description": { + "value": "

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32E87144-8DF8-5270-B5DE-5B940A2F97E0}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-SitecoreContext", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={32B671A7-8840-5D2F-8355-447725C6AEF4})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={21F21053-8F8A-5436-BC79-E674E246A2FC},renderingId={562D5AD8-1E76-5A2B-8161-477852AD7454},id={32B671A7-8840-5D2F-8355-447725C6AEF4})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={21F21053-8F8A-5436-BC79-E674E246A2FC},renderingId={562D5AD8-1E76-5A2B-8161-477852AD7454},id={32B671A7-8840-5D2F-8355-447725C6AEF4})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32B671A7-8840-5D2F-8355-447725C6AEF4}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"562D5AD81E765A2B8161477852AD7454\",\"editable\":\"true\"},\"displayName\":\"Styleguide-RouteFields\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-RouteFields", + "id": "r_21F210538F8A5436BC79E674E246A2FC", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "21f21053-8f8a-5436-bc79-e674e246a2fc", + "componentName": "Styleguide-RouteFields", + "dataSource": "{32B671A7-8840-5D2F-8355-447725C6AEF4}", + "fields": { + "heading": { + "value": "Route-level Fields", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32B671A7-8840-5D2F-8355-447725C6AEF4}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Route-level Fields" + }, + "description": { + "value": "

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{32B671A7-8840-5D2F-8355-447725C6AEF4}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-RouteFields", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={7F56C97D-B7E2-5A83-87C8-C9CABF64A02F})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={A0A66136-C21F-52E8-A2EA-F04DCFA6A027},renderingId={845CD18D-5F81-5B33-8A21-14B518425E16},id={7F56C97D-B7E2-5A83-87C8-C9CABF64A02F})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={A0A66136-C21F-52E8-A2EA-F04DCFA6A027},renderingId={845CD18D-5F81-5B33-8A21-14B518425E16},id={7F56C97D-B7E2-5A83-87C8-C9CABF64A02F})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7F56C97D-B7E2-5A83-87C8-C9CABF64A02F}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"845CD18D5F815B338A2114B518425E16\",\"editable\":\"true\"},\"displayName\":\"Styleguide-ComponentParams\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-ComponentParams", + "id": "r_A0A66136C21F52E8A2EAF04DCFA6A027", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "a0a66136-c21f-52e8-a2ea-f04dcfa6a027", + "componentName": "Styleguide-ComponentParams", + "dataSource": "{7F56C97D-B7E2-5A83-87C8-C9CABF64A02F}", + "params": { + "cssClass": "alert alert-success", + "columns": "5", + "useCallToAction": "true" + }, + "fields": { + "heading": { + "value": "Component Params", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7F56C97D-B7E2-5A83-87C8-C9CABF64A02F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Component Params" + }, + "description": { + "value": "

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{7F56C97D-B7E2-5A83-87C8-C9CABF64A02F}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-ComponentParams", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|description,id={2096063C-413A-574A-8254-6FB1D5FD4883})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={7F765FCB-3B10-58FD-8AA7-B346EF38C9BB},renderingId={86EE9BB7-3508-5B85-A830-9ABFE16346D3},id={2096063C-413A-574A-8254-6FB1D5FD4883})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={7F765FCB-3B10-58FD-8AA7-B346EF38C9BB},renderingId={86EE9BB7-3508-5B85-A830-9ABFE16346D3},id={2096063C-413A-574A-8254-6FB1D5FD4883})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2096063C-413A-574A-8254-6FB1D5FD4883}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"86EE9BB735085B85A8309ABFE16346D3\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Tracking\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Tracking", + "id": "r_7F765FCB3B1058FD8AA7B346EF38C9BB", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "7f765fcb-3b10-58fd-8aa7-b346ef38c9bb", + "componentName": "Styleguide-Tracking", + "dataSource": "{2096063C-413A-574A-8254-6FB1D5FD4883}", + "fields": { + "heading": { + "value": "Tracking", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2096063C-413A-574A-8254-6FB1D5FD4883}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Tracking" + }, + "description": { + "value": "

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{2096063C-413A-574A-8254-6FB1D5FD4883}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Tracking", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading,id={D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={66AF8F03-0B52-5425-A6AF-6FB54F2D64D9},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={66AF8F03-0B52-5425-A6AF-6FB54F2D64D9},renderingId={2BE03932-3CBF-5024-A9D3-29E8D0339010},id={D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2BE039323CBF5024A9D329E8D0339010\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Section", + "id": "r_66AF8F030B525425A6AF6FB54F2D64D9", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "66af8f03-0b52-5425-a6af-6fb54f2d64d9", + "componentName": "Styleguide-Section", + "dataSource": "{D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6}", + "fields": { + "heading": { + "value": "Multilingual Patterns", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{D89BC128-54F9-50E9-B7C9-DFED6AF6F3B6}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Multilingual Patterns" + } + }, + "placeholders": { + "jss-styleguide-section": [ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"030E0BA8B5B8544FB4244BB1D83CDA2E\",\"9D2D9D9CE9A25441926DA6812846C38D\",\"C9269300C2895648B572E6746C801C64\",\"367D6C1DB9985FA1B63DF48D03D2F8BD\",\"2F1D041635E1599D8FA6071EA1EEF2F7\",\"8E7F99EBE0FF5B9F9FD8A9FE0AF250ED\",\"3AAD655F10825CF8B7CEC1CFD15907D0\",\"979DF740D0BE579A9A2A5CEA7F3B2644\",\"7E20A4D5E8A258BB9FC116E9D00910D4\",\"12E44B0639515D3587A7695822E7FF82\",\"F42735C8C3815A6890FDD5B404A0E086\",\"F8D933858F385685A7F7E3AE290E647D\",\"9E93AE70E62657C3B34719C0671EC102\",\"8D929C7673E35409950B9A1A1E2CD98C\",\"562D5AD81E765A2B8161477852AD7454\",\"845CD18D5F815B338A2114B518425E16\",\"86EE9BB735085B85A8309ABFE16346D3\",\"2131857F0CA45425A6A0221BA1F3C12D\"],\"editable\":\"true\"},\"displayName\":\"jss-styleguide-section\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "_jss_main_jss_styleguide_layout__34A6553C_81DE_5CD3_989E_853F6CB6DF8C__0_jss_styleguide_section__66AF8F03_0B52_5425_A6AF_6FB54F2D64D9__0", + "key": "/jss-main/jss-styleguide-layout-{34A6553C-81DE-5CD3-989E-853F6CB6DF8C}-0/jss-styleguide-section-{66AF8F03-0B52-5425-A6AF-6FB54F2D64D9}-0", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=sample|heading|description,id={B2C10553-6A79-5AB2-B182-71313CE56275})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={CF1B5D2B-C949-56E7-9594-66AFACEACA9D},renderingId={2131857F-0CA4-5425-A6A0-221BA1F3C12D},id={B2C10553-6A79-5AB2-B182-71313CE56275})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={CF1B5D2B-C949-56E7-9594-66AFACEACA9D},renderingId={2131857F-0CA4-5425-A6A0-221BA1F3C12D},id={B2C10553-6A79-5AB2-B182-71313CE56275})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B2C10553-6A79-5AB2-B182-71313CE56275}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"2131857F0CA45425A6A0221BA1F3C12D\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Multilingual\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Multilingual", + "id": "r_CF1B5D2BC94956E7959466AFACEACA9D", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "cf1b5d2b-c949-56e7-9594-66afaceaca9d", + "componentName": "Styleguide-Multilingual", + "dataSource": "{B2C10553-6A79-5AB2-B182-71313CE56275}", + "fields": { + "sample": { + "value": "This text can be translated in en.yml", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B2C10553-6A79-5AB2-B182-71313CE56275}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"This field has a translated value\",\"expandedDisplayName\":null}This text can be translated in en.yml" + }, + "heading": { + "value": "Translation Patterns", + "editable": "{\"commands\":[{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B2C10553-6A79-5AB2-B182-71313CE56275}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"heading\",\"expandedDisplayName\":null}Translation Patterns" + }, + "description": { + "value": "", + "editable": "{\"commands\":[{\"click\":\"chrome:field:editcontrol({command:\\\"webedit:edithtml\\\"})\",\"header\":\"Edit Text\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the text\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"bold\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_bold.png\",\"disabledIcon\":\"/temp/font_style_bold_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Bold\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Italic\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_italics.png\",\"disabledIcon\":\"/temp/font_style_italics_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Italic\",\"type\":null},{\"click\":\"chrome:field:execute({command:\\\"Underline\\\", userInterface:true, value:true})\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/font_style_underline.png\",\"disabledIcon\":\"/temp/font_style_underline_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Underline\",\"type\":null},{\"click\":\"chrome:field:insertexternallink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/earth_link.png\",\"disabledIcon\":\"/temp/earth_link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an external link into the text field.\",\"type\":null},{\"click\":\"chrome:field:insertlink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link.png\",\"disabledIcon\":\"/temp/link_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert a link into the text field.\",\"type\":null},{\"click\":\"chrome:field:removelink\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/link_broken.png\",\"disabledIcon\":\"/temp/link_broken_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove link.\",\"type\":null},{\"click\":\"chrome:field:insertimage\",\"header\":\"Insert image\",\"icon\":\"/temp/iconcache/office/16x16/photo_landscape.png\",\"disabledIcon\":\"/temp/photo_landscape_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Insert an image into the text field.\",\"type\":null},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{B2C10553-6A79-5AB2-B182-71313CE56275}?lang=en&ver=1\",\"custom\":{},\"displayName\":\"description\",\"expandedDisplayName\":null}[No text in field]" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Multilingual", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-section", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Section", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "jss-styleguide-layout", + "class": "scpm" + } + } + ] + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Styleguide-Layout", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_", + "chrometype": "placeholder", + "kind": "close", + "hintname": "Main", + "class": "scpm" + } + } + ] + } + }, + "devices": [ + { + "id": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "885b8314-7d8c-4cbb-8000-01421ea8f406", + "instanceId": "43222d12-08c9-453b-ae96-d406ebb95126", + "placeholderKey": "main", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "ce4adcfb-7990-4980-83fb-a00c1e3673db", + "instanceId": "cf044ad9-0332-407a-abde-587214a2c808", + "placeholderKey": "/main/centercolumn", + "parameters": {}, + "caching": {}, + "analytics": {} + }, + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "b343725a-3a93-446e-a9c8-3a2cbd3db489", + "placeholderKey": "/main/centercolumn/content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + }, + { + "id": "46d2f427-4ce5-4e1f-ba10-ef3636f43534", + "layoutId": "14030e9f-ce92-49c6-ad87-7d49b50e42ea", + "placeholders": [], + "renderings": [ + { + "id": "493b3a83-0fa7-4484-8fc9-4680991cf743", + "instanceId": "a08c9132-dbd1-474f-a2ca-6ca26a4aa650", + "placeholderKey": "content", + "parameters": {}, + "caching": {}, + "analytics": {} + } + ] + } + ] + } +} diff --git a/tests/data/json/layoutResponse.json b/tests/data/json/layoutResponse.json new file mode 100644 index 0000000..a7003c0 --- /dev/null +++ b/tests/data/json/layoutResponse.json @@ -0,0 +1 @@ +{"sitecore":{"context":{"pageEditing":false,"site":{"name":"xm-jss-react"},"pageState":"normal","language":"en","itemPath":"/styleguide"},"route":{"name":"styleguide","displayName":"styleguide","fields":{"pageTitle":{"value":"Styleguide | Sitecore JSS"}},"databaseName":"master","deviceId":"fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3","itemId":"3ea3cda2-4122-5580-bae6-a66caa14bd41","itemLanguage":"en","itemVersion":1,"layoutId":"be7ae432-5a8a-59dd-9874-62218b1621a8","templateId":"89829df8-93ee-56be-b57a-8f9efa6ba0a6","templateName":"App Route","placeholders":{"jss-main":[{"uid":"e02ddb9b-a062-5e50-924a-1940d7e053ce","componentName":"ContentBlock","dataSource":"{2FCE1024-EB01-5F0A-93F2-E45A4A8376FE}","params":{},"fields":{"heading":{"value":"更 JSS Styleguide"},"content":{"value":"

This is a live set of examples of how to use JSS. For more information on using JSS, please see the documentation.

\n

The content and layout of this page is defined in /data/routes/styleguide/en.yml

\n"}}},{"uid":"34a6553c-81de-5cd3-989e-853f6cb6df8c","componentName":"Styleguide-Layout","dataSource":"","params":{},"placeholders":{"jss-styleguide-layout":[{"uid":"b7c779da-2b75-586c-b40d-081fcb864256","componentName":"Styleguide-Section","dataSource":"{B52737EC-C248-50F4-B543-2D7D20DD2048}","params":{},"fields":{"heading":{"value":"Content Data"}},"placeholders":{"jss-styleguide-section":[{"uid":"63b0c99e-dac7-5670-9d66-c26a78000eae","componentName":"Styleguide-FieldUsage-Text","dataSource":"{08991556-4DFB-5A82-AEF4-5201DE70809F}","params":{},"fields":{"sample":{"value":"This is a sample text field. HTML is encoded. In Sitecore, editors will see a ."},"sample2":{"value":"This is another sample text field using rendering options. HTML supported with encode=false. Cannot edit because editable=false."},"heading":{"value":"Single-Line Text"},"description":{"value":""}}},{"uid":"f1ea3bb5-1175-5055-ab11-9c48bf69427a","componentName":"Styleguide-FieldUsage-Text","dataSource":"{ADEA93A9-48CB-5703-9981-71CFA0573F72}","params":{},"fields":{"sample":{"value":"This is a sample multi-line text field. HTML is encoded. In Sitecore, editors will see a textarea."},"sample2":{"value":"This is another sample multi-line text field using rendering options. HTML supported with encode=false."},"heading":{"value":"Multi-Line Text"},"description":{"value":"Multi-line text tells Sitecore to use a textarea for editing; consumption in JSS is the same as single-line text."}}},{"uid":"69cebc00-446b-5141-ad1e-450b8d6ee0ad","componentName":"Styleguide-FieldUsage-RichText","dataSource":"{ABEBBCC7-A1C4-5558-BC69-987429546BA6}","params":{},"fields":{"sample":{"value":"

This is a sample rich text field. HTML is always supported. In Sitecore, editors will see a WYSIWYG editor for these fields.

"},"sample2":{"value":"

Another sample rich text field, using options. Keep markup entered in rich text fields as simple as possible - ideally bare tags only (no classes). Adding a wrapping class can help with styling within rich text blocks.

\nBut you can use any valid HTML in a rich text field!\n"},"heading":{"value":"Rich Text"},"description":{"value":""}}},{"uid":"5630c0e6-0430-5f6a-af9e-2d09d600a386","componentName":"Styleguide-FieldUsage-Image","dataSource":"{4A6BE8AB-EBFD-5E36-890C-43297C50794D}","params":{},"fields":{"sample1":{"value":{"src":"https://sc102xm1_jss.cm/-/media/xm-jss-react/data/media/img/sc_logo.ashx?iar=0&hash=D00234D1A3E45010170DBD48EEABD255","alt":"Sitecore Logo"}},"sample2":{"value":{"src":"https://sc102xm1_jss.cm/-/media/xm-jss-react/data/media/img/jss_logo.ashx?iar=0&hash=0D8D5717D85F4846931101C5BB8D64E8","alt":"Sitecore JSS Logo"}},"heading":{"value":"Image"},"description":{"value":""}}},{"uid":"bad43ef7-8940-504d-a09b-976c17a9a30c","componentName":"Styleguide-FieldUsage-File","dataSource":"{794C64EF-E2EE-5C89-B481-5E5DC1451AE1}","params":{},"fields":{"file":{"value":{"src":"https://sc102xm1_jss.cm/-/media/xm-jss-react/data/media/files/jss.ashx","name":"jss","displayName":"jss","title":"Example File","keywords":"","description":"This data will be added to the Sitecore Media Library on import","extension":"pdf","mimeType":"application/pdf","size":"156897"}},"heading":{"value":"File"},"description":{"value":"Note: Sitecore does not support inline editing of File fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"}}},{"uid":"ff90d4bd-e50d-5bbf-9213-d25968c9ae75","componentName":"Styleguide-FieldUsage-Number","dataSource":"{ADCB9DC7-192D-5594-B457-2F53988490CF}","params":{},"fields":{"sample":{"value":1.21},"heading":{"value":"Number"},"description":{"value":"Number tells Sitecore to use a number entry for editing."}}},{"uid":"b5c1c74a-a81d-59b2-85d8-09bc109b1f70","componentName":"Styleguide-FieldUsage-Checkbox","dataSource":"{DBF71434-896D-56C6-A5CA-BB65CD60C20C}","params":{},"fields":{"checkbox":{"value":true},"checkbox2":{"value":false},"heading":{"value":"Checkbox"},"description":{"value":"Note: Sitecore does not support inline editing of Checkbox fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n"}}},{"uid":"f166a7d6-9ec8-5c53-b825-33405db7f575","componentName":"Styleguide-FieldUsage-Date","dataSource":"{992BCFC6-D3AD-53CB-9606-25552646D9C0}","params":{},"fields":{"date":{"value":"2012-05-04T00:00:00Z"},"dateTime":{"value":"2018-03-14T15:00:00Z"},"heading":{"value":"Date"},"description":{"value":"

Both Date and DateTime field types are available. Choosing DateTime will make Sitecore show editing UI for time; both types store complete date and time values internally. Date values in JSS are formatted using ISO 8601 formatted strings, for example 2012-04-23T18:25:43.511Z.

\n
Note: this is a JavaScript date format (e.g. new Date().toISOString()), and is different from how Sitecore stores date field values internally. Sitecore-formatted dates will not work.
\n"}}},{"uid":"56a9562a-6813-579b-8ed2-fddab1bfd3d2","componentName":"Styleguide-FieldUsage-Link","dataSource":"{810247D5-E9C7-5618-9E7C-042A5ECBD888}","params":{},"fields":{"externalLink":{"value":{"href":"https://www.sitecore.com","text":"Link to Sitecore","url":"https://www.sitecore.com","linktype":"external"}},"internalLink":{"value":{"href":"/en/","linktype":"internal","id":"{CA3D5CC0-F313-5D94-BCF3-ECCB6ADE5067}"}},"emailLink":{"value":{"href":"mailto:foo@bar.com","text":"Send an Email","url":"mailto:foo@bar.com","linktype":"mailto"}},"paramsLink":{"value":{"href":"https://dev.sitecore.net","target":"_blank","text":"Sitecore Dev Site","title":" title attribute","url":"https://dev.sitecore.net","class":"font-weight-bold","linktype":"external"}},"heading":{"value":"General Link"},"description":{"value":"

A General Link is a field that represents an <a> tag.

"}}},{"uid":"a44ad1f8-0582-5248-9df9-52429193a68b","componentName":"Styleguide-FieldUsage-ItemLink","dataSource":"{86DC7499-31FD-53EA-997D-9BA4007D39BB}","params":{},"fields":{"sharedItemLink":{"id":"5bc69d7c-a059-55ef-a53f-3f5fedded472","url":"/Content/Styleguide/ItemLinkField/Item1","name":"Item1","displayName":"Styleguide Item Link Item 1 (Shared)","fields":{"textField":{"value":"ItemLink Demo (Shared) Item 1 Text Field"}}},"localItemLink":{"id":"e06f4ce9-36f9-5216-ac6b-e7dbccd13675","url":"/styleguide/Page-Components/styleguide-jss-styleguide-section-B73482E131E5A083D77A50554BC74A4758E29636DF6824F6E2F272EE778C28A095/styleguide-jss-styleguide-section-B75151F05CFDC4CAFFE44E5BAED9D59BEA82565EC11CE75B7DEF3634495EC1DAB7","name":"styleguide-jss-styleguide-section-B75151F05CFDC4CAFFE44E5BAED9D59BEA82565EC11CE75B7DEF3634495EC1DAB7","displayName":"Styleguide-FieldUsage-ItemLink-10-item-0","fields":{"textField":{"value":"Referenced item textField"}}},"heading":{"value":"Item Link"},"description":{"value":"

\n \n Item Links are a way to reference another content item to use data from it.\n Referenced items may be shared.\n To reference multiple content items, use a Content List field.
\n Note: Sitecore does not support inline editing of Item Link fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"}}},{"uid":"2f609d40-8ad9-540e-901e-23aa2600f3eb","componentName":"Styleguide-FieldUsage-ContentList","dataSource":"{2AAFF7A5-5203-5CEA-99E6-72CCD2286ADF}","params":{},"fields":{"sharedContentList":[{"id":"20ee7b13-a7a7-5c34-9b80-dc10248d3b8a","url":"/Content/Styleguide/ContentListField/Item1","name":"Item1","displayName":"Styleguide Content List Item 1 (Shared)","fields":{"textField":{"value":"ContentList Demo (Shared) Item 1 Text Field"}}},{"id":"e170308c-cc35-5eaa-a0cc-98cd1b02b457","url":"/Content/Styleguide/ContentListField/Item2","name":"Item2","displayName":"Styleguide Content List Item 2 (Shared)","fields":{"textField":{"value":"ContentList Demo (Shared) Item 2 Text Field"}}}],"localContentList":[{"id":"9df83293-9f68-5f75-a667-ad86aa0f12c2","url":"/styleguide/Page-Components/styleguide-jss-styleguide-section-B7985C8DEA10AB2C5CC77ABC90CF4126F21840592E09B2C6DB07D84D314CDCE0D4/styleguide-jss-styleguide-section-B7BF1F3509A82A38C71B36F25C4E58E2D04EC965C308A1147E993853137210F241","name":"styleguide-jss-styleguide-section-B7BF1F3509A82A38C71B36F25C4E58E2D04EC965C308A1147E993853137210F241","displayName":"Styleguide-FieldUsage-ContentList-11-item-0","fields":{"textField":{"value":"Hello World Item 1"}}},{"id":"cf304c9f-a7a9-587d-8267-c421f6a5335d","url":"/styleguide/Page-Components/styleguide-jss-styleguide-section-B7985C8DEA10AB2C5CC77ABC90CF4126F21840592E09B2C6DB07D84D314CDCE0D4/styleguide-jss-styleguide-section-B76CB8FB6A660296D91AE50FD7559AF62453A1891C9828A22C84E7E2A9914E7DD8","name":"styleguide-jss-styleguide-section-B76CB8FB6A660296D91AE50FD7559AF62453A1891C9828A22C84E7E2A9914E7DD8","displayName":"Styleguide-FieldUsage-ContentList-11-item-1","fields":{"textField":{"value":"Hello World Item 2"}}}],"heading":{"value":"Content List"},"description":{"value":"

\n \n Content Lists are a way to reference zero or more other content items.\n Referenced items may be shared.\n To reference a single content item, use an Item Link field.
\n Note: Sitecore does not support inline editing of Content List fields. The value must be edited in Experience Editor by using the edit rendering fields button (looks like a pencil) with the whole component selected.\n
\n

\n"}}},{"uid":"352ed63d-796a-5523-89f5-9a991dda4a8f","componentName":"Styleguide-FieldUsage-Custom","dataSource":"{F058DA49-A353-5C15-82A5-F80AA5F9CF97}","params":{},"fields":{"customIntField":{"value":31337},"heading":{"value":"Custom Fields"},"description":{"value":"

\n \n Any Sitecore field type can be consumed by JSS.\n In this sample we consume the Integer field type.
\n Note: For field types with complex data, custom FieldSerializers may need to be implemented on the Sitecore side.\n
\n

\n"}}}]}},{"uid":"7de41a1a-24e4-5963-8206-3bb0b7d9dd69","componentName":"Styleguide-Section","dataSource":"{EAB74A15-0EA7-53F1-B80B-5603C413977D}","params":{},"fields":{"heading":{"value":"Layout Patterns"}},"placeholders":{"jss-styleguide-section":[{"uid":"3a5d9c50-d8c1-5a12-8da8-5d56c2a5a69a","componentName":"Styleguide-Layout-Reuse","dataSource":"{CED00D9A-267D-5206-A538-4B05E9905212}","params":{},"fields":{"heading":{"value":"Reusing Content"},"description":{"value":"

JSS provides powerful options to reuse content, whether it's sharing a common piece of text across pages or sketching out a site with repeating lorem ipsum content.

"}},"placeholders":{"jss-reuse-example":[{"uid":"aa328b8a-d6e1-5b37-8143-250d2e93d6b8","componentName":"ContentBlock","dataSource":"{76BBC05D-2575-5565-A301-478843FD05F6}","params":{},"fields":{"heading":{"value":""},"content":{"value":"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

"}}},{"uid":"c4330d34-623c-556c-bf4c-97c93d40fb1e","componentName":"ContentBlock","dataSource":"{76BBC05D-2575-5565-A301-478843FD05F6}","params":{},"fields":{"heading":{"value":""},"content":{"value":"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

"}}},{"uid":"a42d8b1c-193d-5627-9130-f7f7f87617f1","componentName":"ContentBlock","dataSource":"{EF974BD3-48C3-52A0-894E-595C44269D4F}","params":{},"fields":{"heading":{"value":""},"content":{"value":"

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque felis mauris, pretium id neque vitae, vulputate pellentesque tortor. Mauris hendrerit dolor et ipsum lobortis bibendum non finibus neque. Morbi volutpat aliquam magna id posuere. Duis commodo cursus dui, nec interdum velit congue nec. Aliquam erat volutpat. Aliquam facilisis, sapien quis fringilla tincidunt, magna nulla feugiat neque, a consectetur arcu orci eu augue.

"}}},{"uid":"0f4cb47a-979e-5139-b50b-a8e40c73c236","componentName":"ContentBlock","dataSource":"{5336630F-66AA-50F3-9D50-ABC3D2DB67E9}","params":{},"fields":{"heading":{"value":""},"content":{"value":"

Mix and match reused and local content. Check out /data/routes/styleguide/en.yml to see how.

"}}}]}},{"uid":"538e4831-f157-50bb-ac74-277fcac9fddb","componentName":"Styleguide-Layout-Tabs","dataSource":"{269200EA-745F-52F0-B482-38587CE1B2DA}","params":{},"fields":{"heading":{"value":"Tabs"},"description":{"value":"

Creating hierarchical components like tabs is made simpler in JSS because it's easy to introspect the layout structure.

"}},"placeholders":{"jss-tabs":[{"uid":"7ecb2ed2-ac9b-58d1-8365-10ca74824af7","componentName":"Styleguide-Layout-Tabs-Tab","dataSource":"{0369F270-F5B0-54B7-A4CE-ECD6968A0E61}","params":{},"fields":{"title":{"value":"Tab 1"},"content":{"value":"

Tab 1 contents!

"}}},{"uid":"afd64900-0a61-50eb-a674-a7a884e0d496","componentName":"Styleguide-Layout-Tabs-Tab","dataSource":"{DD5B0FF3-6686-5A8B-A51C-1450B352950C}","params":{},"fields":{"title":{"value":"Tab 2"},"content":{"value":"

Tab 2 contents!

"}}},{"uid":"44c12983-3a84-5462-84c0-6ca1430050c8","componentName":"Styleguide-Layout-Tabs-Tab","dataSource":"{1850FC78-5940-5AAC-A95C-D5FD2B5627E7}","params":{},"fields":{"title":{"value":"Tab 3"},"content":{"value":"

Tab 3 contents!

"}}}]}}]}},{"uid":"2d806c25-dd46-51e3-93de-63cf9035122c","componentName":"Styleguide-Section","dataSource":"{F63B072C-D29C-5A14-83A9-B7F8BAF6D349}","params":{},"fields":{"heading":{"value":"Sitecore Patterns"}},"placeholders":{"jss-styleguide-section":[{"uid":"471fa16a-bb82-5c42-9c95-e7eab1e3bd30","componentName":"Styleguide-SitecoreContext","dataSource":"{54BA2BA0-CE7B-5B29-A9D4-6EA3739CDC33}","params":{},"fields":{"heading":{"value":"Sitecore Context"},"description":{"value":"

The Sitecore Context contains route-level data about the current context - for example, pageState enables conditionally executing code based on whether Sitecore is in Experience Editor or not.

"}}},{"uid":"21f21053-8f8a-5436-bc79-e674e246a2fc","componentName":"Styleguide-RouteFields","dataSource":"{C0ECA911-DCBB-5BB5-A359-3911D217A47B}","params":{},"fields":{"heading":{"value":"Route-level Fields"},"description":{"value":"

Route-level content fields are defined on the route instead of on a component. This allows multiple components to share the field data on the same route - and querying is much easier on route level fields, making custom route types ideal for filterable/queryable data such as articles.

"}}},{"uid":"a0a66136-c21f-52e8-a2ea-f04dcfa6a027","componentName":"Styleguide-ComponentParams","dataSource":"{7ABE7716-4B28-58DC-BCFA-5F7EBC891FAD}","params":{"cssClass":"alert alert-success","columns":"5","useCallToAction":"true"},"fields":{"heading":{"value":"Component Params"},"description":{"value":"

Component params (also called Rendering Parameters) allow storing non-content parameters for a component. These params should be used for more technical options such as CSS class names or structural settings.

"}}},{"uid":"7f765fcb-3b10-58fd-8aa7-b346ef38c9bb","componentName":"Styleguide-Tracking","dataSource":"{5A832082-7BDD-546F-A3D7-7B976478288F}","params":{},"fields":{"heading":{"value":"Tracking"},"description":{"value":"

JSS supports tracking Sitecore analytics events from within apps. Give it a try with this handy interactive demo.

"}}}]}},{"uid":"66af8f03-0b52-5425-a6af-6fb54f2d64d9","componentName":"Styleguide-Section","dataSource":"{DAF845FB-08DA-50D0-B7CD-BCE717583A8D}","params":{},"fields":{"heading":{"value":"Multilingual Patterns"}},"placeholders":{"jss-styleguide-section":[{"uid":"cf1b5d2b-c949-56e7-9594-66afaceaca9d","componentName":"Styleguide-Multilingual","dataSource":"{6E7D97B4-4C61-5EB9-8D3E-1C85594F1C41}","params":{},"fields":{"sample":{"value":"This text can be translated in en.yml"},"heading":{"value":"Translation Patterns"},"description":{"value":""}}}]}}]}}]}}}} diff --git a/tests/data/json/mixedComponentsEditChromes.json b/tests/data/json/mixedComponentsEditChromes.json new file mode 100644 index 0000000..e78327e --- /dev/null +++ b/tests/data/json/mixedComponentsEditChromes.json @@ -0,0 +1,75 @@ +[ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "jss_main", + "key": "jss-main", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_E02DDB9BA0625E50924A1940D7E053CE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "uid": "e02ddb9b-a062-5e50-924a-1940d7e053ce", + "componentName": "ContentBlock", + "dataSource": "{585596CA-7903-500B-8DF2-0357DD6E3FAC}", + "fields": { + "heading": { + "value": "Example heading" + }, + "content": { + "value": "Example content" + } + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "uid": "34a6553c-81de-5cd3-989e-853f6cb6df8c", + "componentName": "Styleguide-Layout", + "dataSource": "" + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"4E3C94B3A9D25478B7548D87283D8AA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout", + "id": "r_34A6553C81DE5CD3989E853F6CB6DF8C", + "class": "scpm", + "data-selectable": "true" + } + } +] diff --git a/tests/data/json/onlyComponents.json b/tests/data/json/onlyComponents.json new file mode 100644 index 0000000..6159bc3 --- /dev/null +++ b/tests/data/json/onlyComponents.json @@ -0,0 +1,20 @@ +[ + { + "uid": "e02ddb9b-a062-5e50-924a-1940d7e053ce", + "componentName": "ContentBlock", + "dataSource": "{585596CA-7903-500B-8DF2-0357DD6E3FAC}", + "fields": { + "heading": { + "value": "Example heading" + }, + "content": { + "value": "Example content" + } + } + }, + { + "uid": "34a6553c-81de-5cd3-989e-853f6cb6df8c", + "componentName": "Styleguide-Layout", + "dataSource": "" + } +] diff --git a/tests/data/json/onlyEditChromes.json b/tests/data/json/onlyEditChromes.json new file mode 100644 index 0000000..38b58b3 --- /dev/null +++ b/tests/data/json/onlyEditChromes.json @@ -0,0 +1,57 @@ +[ + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:placeholder:addControl\",\"header\":\"Add to here\",\"icon\":\"/temp/iconcache/office/16x16/add.png\",\"disabledIcon\":\"/temp/add_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Add a new rendering to the '{0}' placeholder.\",\"type\":\"\"},{\"click\":\"chrome:placeholder:editSettings\",\"header\":\"\",\"icon\":\"/temp/iconcache/office/16x16/window_gear.png\",\"disabledIcon\":\"/temp/window_gear_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the placeholder settings.\",\"type\":\"\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"allowedRenderings\":[\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"4E3C94B3A9D25478B7548D87283D8AA6\",\"26D9B310A5365D6B975442DB6BE1D381\",\"27EA18D87B6456108919947077956819\"],\"editable\":\"true\"},\"displayName\":\"Main\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "placeholder", + "kind": "open", + "id": "jss_main", + "key": "jss-main", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={2F7D34E2-EB93-47ED-8177-B1F429A9FCD7},fields=heading|content,id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Fields\",\"icon\":\"/temp/iconcache/office/16x16/pencil.png\",\"disabledIcon\":\"/temp/pencil_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"\",\"type\":null},{\"click\":\"chrome:dummy\",\"header\":\"Separator\",\"icon\":\"\",\"disabledIcon\":\"\",\"isDivider\":false,\"tooltip\":null,\"type\":\"separator\"},{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={E02DDB9B-A062-5E50-924A-1940D7E053CE},renderingId={1DE91AAD-C146-5D89-83FA-31A8FD63EBB3},id={585596CA-7903-500B-8DF2-0357DD6E3FAC})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{585596CA-7903-500B-8DF2-0357DD6E3FAC}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"1DE91AADC1465D8983FA31A8FD63EBB3\",\"editable\":\"true\"},\"displayName\":\"Content Block\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Content Block", + "id": "r_E02DDB9BA0625E50924A1940D7E053CE", + "class": "scpm", + "data-selectable": "true" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "", + "attributes": { + "type": "text/sitecore", + "id": "scEnclosingTag_r_", + "chrometype": "rendering", + "kind": "close", + "hintkey": "Content Block", + "class": "scpm" + } + }, + { + "name": "code", + "type": "text/sitecore", + "contents": "{\"commands\":[{\"click\":\"chrome:rendering:sort\",\"header\":\"Change position\",\"icon\":\"/temp/iconcache/office/16x16/document_size.png\",\"disabledIcon\":\"/temp/document_size_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Move component.\",\"type\":\"\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:componentoptions(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"Edit Experience Editor Options\",\"icon\":\"/temp/iconcache/office/16x16/clipboard_check_edit.png\",\"disabledIcon\":\"/temp/clipboard_check_edit_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the Experience Editor options for the component.\",\"type\":\"common\"},{\"click\":\"chrome:rendering:properties\",\"header\":\"Edit component properties\",\"icon\":\"/temp/iconcache/office/16x16/elements_branch.png\",\"disabledIcon\":\"/temp/elements_branch_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the properties for the component.\",\"type\":\"common\"},{\"click\":\"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:setdatasource(referenceId={34A6553C-81DE-5CD3-989E-853F6CB6DF8C},renderingId={4E3C94B3-A9D2-5478-B754-8D87283D8AA6},id={616E2DAA-BB71-5117-82B1-B360EF600213})',null,false)\",\"header\":\"dsHeaderParameter\",\"icon\":\"/temp/iconcache/office/16x16/data.png\",\"disabledIcon\":\"/temp/data_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"dsTooltipParameter\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:personalize({command:\\\"webedit:personalize\\\"})\",\"header\":\"Personalize\",\"icon\":\"/temp/iconcache/office/16x16/users_family.png\",\"disabledIcon\":\"/temp/users_family_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Create or edit personalization for this component.\",\"type\":\"sticky\"},{\"click\":\"chrome:common:edititem({command:\\\"webedit:open\\\"})\",\"header\":\"Edit the related item\",\"icon\":\"/temp/iconcache/office/16x16/cubes.png\",\"disabledIcon\":\"/temp/cubes_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Edit the related item in the Content Editor.\",\"type\":\"datasourcesmenu\"},{\"click\":\"chrome:rendering:delete\",\"header\":\"Delete\",\"icon\":\"/temp/iconcache/office/16x16/delete.png\",\"disabledIcon\":\"/temp/delete_disabled16x16.png\",\"isDivider\":false,\"tooltip\":\"Remove component.\",\"type\":\"sticky\"}],\"contextItemUri\":\"sitecore://master/{616E2DAA-BB71-5117-82B1-B360EF600213}?lang=en&ver=1\",\"custom\":{\"renderingID\":\"4E3C94B3A9D25478B7548D87283D8AA6\",\"editable\":\"true\"},\"displayName\":\"Styleguide-Layout\",\"expandedDisplayName\":null}", + "attributes": { + "type": "text/sitecore", + "chrometype": "rendering", + "kind": "open", + "hintname": "Styleguide-Layout", + "id": "r_34A6553C81DE5CD3989E853F6CB6DF8C", + "class": "scpm", + "data-selectable": "true" + } + } +]