diff --git a/.editorconfig b/.editorconfig index 444a1d4..32c153d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -64,7 +64,7 @@ csharp_preserve_single_line_statements = fals csharp_preserve_single_line_blocks = true ### Using directive options -csharp_using_directive_placement = outside_namespace:error +csharp_using_directive_placement = outside_namespace : error dotnet_diagnostic.IDE0065.severity = error # Code Style Rules @@ -82,7 +82,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true dotnet_style_predefined_type_for_member_access = true : error dotnet_diagnostic.IDE0049.severity = error -dotnet_style_require_accessibility_modifiers = always : error +dotnet_style_require_accessibility_modifiers = for_non_interface_members : error dotnet_diagnostic.IDE0040.severity = error dotnet_style_readonly_field = true : error @@ -210,29 +210,241 @@ dotnet_diagnostic.IDE0062.severity = warn csharp_style_inlined_variable_declaration = true : error dotnet_diagnostic.IDE0018.severity = error -csharp_style_expression_bodied_constructors = true:error +# Severity levels of analyzers https://docs.microsoft.com/en-us/visualstudio/code-quality/roslyn-analyzers-overview?view=vs-2019#severity-levels-of-analyzers + +root = true + +[*.cs] +end_of_line = crlf +indent_size = 4 +indent_style = space +insert_final_newline = true + +# Formatting Rules + +## IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = error + +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + +## C# Formatting Rules https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules + +### Newline options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +### Indentation options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false + +### Spacing options +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +### Wrap options +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +### Using directive options +csharp_using_directive_placement = outside_namespace : error +dotnet_diagnostic.IDE0065.severity = error + +# Code Style Rules + +## .NET Code Style + +dotnet_style_qualification_for_event = false : error +dotnet_style_qualification_for_field = false : error +dotnet_style_qualification_for_method = false : error +dotnet_style_qualification_for_property = false : error +dotnet_diagnostic.IDE0003.severity = error +dotnet_diagnostic.IDE0009.severity = error + +dotnet_style_predefined_type_for_locals_parameters_members = true : error +dotnet_style_predefined_type_for_member_access = true : error +dotnet_diagnostic.IDE0049.severity = error + +dotnet_style_require_accessibility_modifiers = for_non_interface_members : error +dotnet_diagnostic.IDE0040.severity = error + +dotnet_style_readonly_field = true : error +dotnet_diagnostic.IDE0044.severity = error + +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity : warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity : warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity : warning +dotnet_style_parentheses_in_other_operators = always_for_clarity : warning +dotnet_diagnostic.IDE0047.severity = warning +dotnet_diagnostic.IDE0048.severity = warning + +dotnet_style_object_initializer = true : error +dotnet_diagnostic.IDE0017.severity = error + +dotnet_style_explicit_tuple_names = true : error +dotnet_diagnostic.IDE0033.severity = error + +csharp_prefer_simple_default_expression = true : error +dotnet_diagnostic.IDE0034.severity = error + +dotnet_style_prefer_inferred_tuple_names = true : error +dotnet_style_prefer_inferred_anonymous_type_member_names = true : error +dotnet_diagnostic.IDE0037.severity = error + +dotnet_style_prefer_conditional_expression_over_assignment = true : error +dotnet_diagnostic.IDE0045.severity = error + +dotnet_style_prefer_conditional_expression_over_return = true : silent +dotnet_diagnostic.IDE0046.severity = refactoring + +dotnet_style_prefer_compound_assignment = true : error +dotnet_diagnostic.IDE0054.severity = error +dotnet_diagnostic.IDE0074.severity = error + +dotnet_style_prefer_simplified_boolean_expressions = true : warning +dotnet_diagnostic.IDE0075.severity = warning + +dotnet_style_coalesce_expression = true : error +dotnet_diagnostic.IDE0029.severity = error +dotnet_diagnostic.IDE0030.severity = error + +dotnet_style_null_propagation = true : error +dotnet_diagnostic.IDE0031.severity = error + +dotnet_style_prefer_is_null_check_over_reference_equality_method = true : error +dotnet_diagnostic.IDE0041.severity = error + +dotnet_style_collection_initializer = true : error +dotnet_diagnostic.IDE0028.severity = error + +dotnet_style_prefer_auto_properties = true : warning +dotnet_diagnostic.IDE0032.severity = warning + +dotnet_code_quality_unused_parameters = all : error +dotnet_diagnostic.IDE0060.severity = error + +dotnet_remove_unnecessary_suppression_exclusions = none : warning +dotnet_diagnostic.IDE0079.severity = warning + +dotnet_style_prefer_simplified_interpolation = true : error +dotnet_diagnostic.IDE0071.severity = error + +## Rules without Style Options + +# IDE0010: Add missing cases +dotnet_diagnostic.IDE0010.severity = error + +# IDE0001: Simplify name +dotnet_diagnostic.IDE0001.severity = error + +# IDE0002: Simplify member access +dotnet_diagnostic.IDE0002.severity = error + +# IDE0004: Remove unnecessary cast +dotnet_diagnostic.IDE0004.severity = error + +# IDE0005: Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = error + +# IDE0100: Remove redundant equality +dotnet_diagnostic.IDE0100.severity = error + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0058: Remove unnecessary expression value +dotnet_diagnostic.IDE0058.severity = refactoring + +# IDE0059: Remove unnecessary value assignment +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0070: Use 'System.HashCode.Combine' +dotnet_diagnostic.IDE0070.severity = error + +## C# Code style + +csharp_style_pattern_local_over_anonymous_function = true : suggestion +dotnet_diagnostic.IDE0039.severity = refactoring + +csharp_style_deconstructed_variable_declaration = true : warning +dotnet_diagnostic.IDE0042.severity = warning + +csharp_style_implicit_object_creation_when_type_is_apparent = true +dotnet_diagnostic.IDE0090.severity = error + +csharp_style_conditional_delegate_call = true : error +dotnet_diagnostic.IDE1005.severity = error + +csharp_style_throw_expression = true : error +dotnet_diagnostic.IDE0016.severity = error + +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async : warning +dotnet_diagnostic.IDE0036.severity = warning + +csharp_prefer_static_local_function = true +dotnet_diagnostic.IDE0062.severity = warning + +csharp_style_inlined_variable_declaration = true : error +dotnet_diagnostic.IDE0018.severity = error + +csharp_style_expression_bodied_constructors = true : error dotnet_diagnostic.IDE0021.severity = error -csharp_style_expression_bodied_methods = true:error +csharp_style_expression_bodied_methods = true : error dotnet_diagnostic.IDE0022.severity = error -csharp_style_expression_bodied_operators = true:error +csharp_style_expression_bodied_operators = true : error dotnet_diagnostic.IDE0023.severity = error dotnet_diagnostic.IDE0024.severity = error -csharp_style_expression_bodied_properties = true:error +csharp_style_expression_bodied_properties = true : error dotnet_diagnostic.IDE0025.severity = error -csharp_style_expression_bodied_indexers = true:error +csharp_style_expression_bodied_indexers = true : error dotnet_diagnostic.IDE0026.severity = error -csharp_style_expression_bodied_accessors = true:error +csharp_style_expression_bodied_accessors = true : error dotnet_diagnostic.IDE0027.severity = error -csharp_style_expression_bodied_lambdas = true:error +csharp_style_expression_bodied_lambdas = true : error dotnet_diagnostic.IDE0053.severity = error -csharp_style_expression_bodied_local_functions = true:error +csharp_style_expression_bodied_local_functions = true : error dotnet_diagnostic.IDE0061.severity = error csharp_style_pattern_matching_over_as_with_null_check = true : error @@ -250,10 +462,10 @@ dotnet_diagnostic.IDE0078.severity = erro csharp_style_prefer_not_pattern = true : error dotnet_diagnostic.IDE0083.severity = error -csharp_prefer_braces = true:error +csharp_prefer_braces = true : error dotnet_diagnostic.IDE0011.severity = error -csharp_prefer_simple_using_statement = true:error +csharp_prefer_simple_using_statement = true : error dotnet_diagnostic.IDE0063.severity = error csharp_style_prefer_index_operator = true : warning @@ -262,6 +474,142 @@ dotnet_diagnostic.IDE0056.severity = warn csharp_style_prefer_range_operator = true : warning dotnet_diagnostic.IDE0057.severity = warning +csharp_style_namespace_declarations = file_scoped : error +dotnet_diagnostic.IDE0161.severity = error + +csharp_style_prefer_null_check_over_type_check = true : warning +dotnet_diagnostic.IDE0150.severity = error + +## Rules without Style Options + +# IDE0050: Convert anonymous type to tuple +dotnet_diagnostic.IDE0050.severity = warning + +# IDE0064: Make readonly fields writable +dotnet_diagnostic.IDE0064.severity = error + +# IDE0072: Add missing cases to switch expression +dotnet_diagnostic.IDE0072.severity = error + +# IDE0082: Convert typeof to nameof +dotnet_diagnostic.IDE0082.severity = error + +# IDE0080: Remove unnecessary suppression operator +dotnet_diagnostic.IDE0080.severity = error + +# IDE0110: Remove unnecessary discard +dotnet_diagnostic.IDE0110.severity = warning + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = error + +# Naming Conventions +dotnet_naming_symbols.const_field_symbols.applicable_kinds = field +dotnet_naming_symbols.const_field_symbols.required_modifiers = const +dotnet_naming_symbols.const_field_symbols.applicable_accessibilities = * +dotnet_naming_style.const_field_symbols.capitalization = pascal_case + +dotnet_naming_rule.const_fields_must_be_pascal_case.severity = error +dotnet_naming_rule.const_fields_must_be_pascal_case.symbols = const_field_symbols +dotnet_naming_rule.const_fields_must_be_pascal_case.style = const_field_symbols + +dotnet_naming_symbols.private_field_symbol.applicable_kinds = field +dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private +dotnet_naming_style.private_field_style.capitalization = camel_case +dotnet_naming_rule.private_fields_are_camel_case.severity = warning +dotnet_naming_rule.private_fields_are_camel_case.symbols = private_field_symbol +dotnet_naming_rule.private_fields_are_camel_case.style = private_field_style + +dotnet_naming_symbols.non_private_field_symbol.applicable_kinds = field +dotnet_naming_symbols.non_private_field_symbol.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend +dotnet_naming_style.non_private_field_style.capitalization = pascal_case +dotnet_naming_rule.non_private_fields_are_pascal_case.severity = warning +dotnet_naming_rule.non_private_fields_are_pascal_case.symbols = non_private_field_symbol +dotnet_naming_rule.non_private_fields_are_pascal_case.style = non_private_field_style + +dotnet_naming_symbols.parameter_symbol.applicable_kinds = parameter +dotnet_naming_style.parameter_style.capitalization = camel_case +dotnet_naming_rule.parameters_are_camel_case.severity = warning +dotnet_naming_rule.parameters_are_camel_case.symbols = parameter_symbol +dotnet_naming_rule.parameters_are_camel_case.style = parameter_style + +dotnet_naming_symbols.non_interface_type_symbol.applicable_kinds = class,struct,enum,delegate +dotnet_naming_style.non_interface_type_style.capitalization = pascal_case +dotnet_naming_rule.non_interface_types_are_pascal_case.severity = error +dotnet_naming_rule.non_interface_types_are_pascal_case.symbols = non_interface_type_symbol +dotnet_naming_rule.non_interface_types_are_pascal_case.style = non_interface_type_style + +dotnet_naming_symbols.interface_type_symbol.applicable_kinds = interface +dotnet_naming_style.interface_type_style.capitalization = pascal_case +dotnet_naming_style.interface_type_style.required_prefix = I +dotnet_naming_rule.interface_types_must_be_prefixed_with_I.severity = error +dotnet_naming_rule.interface_types_must_be_prefixed_with_I.symbols = interface_type_symbol +dotnet_naming_rule.interface_types_must_be_prefixed_with_I.style = interface_type_style + +dotnet_naming_symbols.member_symbol.applicable_kinds = method,property,event +dotnet_naming_style.member_style.capitalization = pascal_case +dotnet_naming_rule.members_are_pascal_case.severity = error +dotnet_naming_rule.members_are_pascal_case.symbols = member_symbol +dotnet_naming_rule.members_are_pascal_case.style = member_style + +csharp_style_expression_bodied_constructors = true : error +dotnet_diagnostic.IDE0021.severity = error + +csharp_style_expression_bodied_methods = true : error +dotnet_diagnostic.IDE0022.severity = error + +csharp_style_expression_bodied_operators = true : error +dotnet_diagnostic.IDE0023.severity = error +dotnet_diagnostic.IDE0024.severity = error + +csharp_style_expression_bodied_properties = true : error +dotnet_diagnostic.IDE0025.severity = error + +csharp_style_expression_bodied_indexers = true : error +dotnet_diagnostic.IDE0026.severity = error + +csharp_style_expression_bodied_accessors = true : error +dotnet_diagnostic.IDE0027.severity = error + +csharp_style_expression_bodied_lambdas = true : error +dotnet_diagnostic.IDE0053.severity = error + +csharp_style_expression_bodied_local_functions = true : error +dotnet_diagnostic.IDE0061.severity = error + +csharp_style_pattern_matching_over_as_with_null_check = true : error +dotnet_diagnostic.IDE0019.severity = error + +csharp_style_pattern_matching_over_is_with_cast_check = true : error +dotnet_diagnostic.IDE0020.severity = error + +csharp_style_prefer_switch_expression = true : error +dotnet_diagnostic.IDE0066.severity = error + +csharp_style_prefer_pattern_matching = true : error +dotnet_diagnostic.IDE0078.severity = error + +csharp_style_prefer_not_pattern = true : error +dotnet_diagnostic.IDE0083.severity = error + +csharp_prefer_braces = true : error +dotnet_diagnostic.IDE0011.severity = error + +csharp_prefer_simple_using_statement = true : error +dotnet_diagnostic.IDE0063.severity = error + +csharp_style_prefer_index_operator = true : warning +dotnet_diagnostic.IDE0056.severity = warning + +csharp_style_prefer_range_operator = true : warning +dotnet_diagnostic.IDE0057.severity = warning + +csharp_style_namespace_declarations = file_scoped : error +dotnet_diagnostic.IDE0161.severity = error + +csharp_style_prefer_null_check_over_type_check = true : warning +dotnet_diagnostic.IDE0150.severity = error + ## Rules without Style Options # IDE0050: Convert anonymous type to tuple @@ -291,70 +639,45 @@ dotnet_naming_symbols.const_field_symbols.required_modifiers = cons dotnet_naming_symbols.const_field_symbols.applicable_accessibilities = * dotnet_naming_style.const_field_symbols.capitalization = pascal_case -dotnet_naming_rule.const_fields_must_be_pascal_case.severity = error +dotnet_naming_rule.const_fields_must_be_pascal_case.severity = error dotnet_naming_rule.const_fields_must_be_pascal_case.symbols = const_field_symbols -dotnet_naming_rule.const_fields_must_be_pascal_case.style = const_field_symbols +dotnet_naming_rule.const_fields_must_be_pascal_case.style = const_field_symbols dotnet_naming_symbols.private_field_symbol.applicable_kinds = field dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private dotnet_naming_style.private_field_style.capitalization = camel_case -dotnet_naming_rule.private_fields_are_camel_case.severity = warning +dotnet_naming_rule.private_fields_are_camel_case.severity = warning dotnet_naming_rule.private_fields_are_camel_case.symbols = private_field_symbol -dotnet_naming_rule.private_fields_are_camel_case.style = private_field_style +dotnet_naming_rule.private_fields_are_camel_case.style = private_field_style dotnet_naming_symbols.non_private_field_symbol.applicable_kinds = field dotnet_naming_symbols.non_private_field_symbol.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend dotnet_naming_style.non_private_field_style.capitalization = pascal_case -dotnet_naming_rule.non_private_fields_are_pascal_case.severity = warning +dotnet_naming_rule.non_private_fields_are_pascal_case.severity = warning dotnet_naming_rule.non_private_fields_are_pascal_case.symbols = non_private_field_symbol -dotnet_naming_rule.non_private_fields_are_pascal_case.style = const_field_symbols +dotnet_naming_rule.non_private_fields_are_pascal_case.style = non_private_field_style dotnet_naming_symbols.parameter_symbol.applicable_kinds = parameter dotnet_naming_style.parameter_style.capitalization = camel_case -dotnet_naming_rule.parameters_are_camel_case.severity = warning +dotnet_naming_rule.parameters_are_camel_case.severity = warning dotnet_naming_rule.parameters_are_camel_case.symbols = parameter_symbol -dotnet_naming_rule.parameters_are_camel_case.style = private_field_style +dotnet_naming_rule.parameters_are_camel_case.style = parameter_style dotnet_naming_symbols.non_interface_type_symbol.applicable_kinds = class,struct,enum,delegate dotnet_naming_style.non_interface_type_style.capitalization = pascal_case -dotnet_naming_rule.non_interface_types_are_pascal_case.severity = error +dotnet_naming_rule.non_interface_types_are_pascal_case.severity = error dotnet_naming_rule.non_interface_types_are_pascal_case.symbols = non_interface_type_symbol -dotnet_naming_rule.non_interface_types_are_pascal_case.style = const_field_symbols +dotnet_naming_rule.non_interface_types_are_pascal_case.style = non_interface_type_style dotnet_naming_symbols.interface_type_symbol.applicable_kinds = interface dotnet_naming_style.interface_type_style.capitalization = pascal_case dotnet_naming_style.interface_type_style.required_prefix = I -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = error +dotnet_naming_rule.interface_types_must_be_prefixed_with_I.severity = error dotnet_naming_rule.interface_types_must_be_prefixed_with_I.symbols = interface_type_symbol -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = interface_type_style +dotnet_naming_rule.interface_types_must_be_prefixed_with_I.style = interface_type_style dotnet_naming_symbols.member_symbol.applicable_kinds = method,property,event dotnet_naming_style.member_style.capitalization = pascal_case -dotnet_naming_rule.members_are_pascal_case.severity = error +dotnet_naming_rule.members_are_pascal_case.severity = error dotnet_naming_rule.members_are_pascal_case.symbols = member_symbol -dotnet_naming_rule.members_are_pascal_case.style = const_field_symbols -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion - -[*.{cs,vb}] -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:error -dotnet_style_null_propagation = true:error -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_object_initializer = true:error -dotnet_style_prefer_collection_expression = true:suggestion -dotnet_style_collection_initializer = true:error -dotnet_style_prefer_simplified_boolean_expressions = true:warning -dotnet_style_prefer_conditional_expression_over_assignment = true:error -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:error -dotnet_style_prefer_inferred_tuple_names = true:error -dotnet_style_prefer_inferred_anonymous_type_member_names = true:error -dotnet_style_prefer_compound_assignment = true:error -dotnet_style_prefer_simplified_interpolation = true:error +dotnet_naming_rule.members_are_pascal_case.style = member_style diff --git a/Directory.Build.props b/Directory.Build.props index de5d862..a12418c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,6 +32,7 @@ true $(NoWarn);1591;S3267 false + disable diff --git a/Directory.Packages.props b/Directory.Packages.props index f1fa140..943cee6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,12 @@ true - - - - + + + + + diff --git a/examples/DancingGoat/.config/dotnet-tools.json b/examples/DancingGoat/.config/dotnet-tools.json index d6bdc96..37083ad 100644 --- a/examples/DancingGoat/.config/dotnet-tools.json +++ b/examples/DancingGoat/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "kentico.xperience.dbmanager": { - "version": "29.5.3", + "version": "30.6.0", "commands": [ "kentico-xperience-dbmanager" ] diff --git a/examples/DancingGoat/AdminComponents/UIPages/UniqueProductSkuValidationRule.cs b/examples/DancingGoat/AdminComponents/UIPages/UniqueProductSkuValidationRule.cs new file mode 100644 index 0000000..acea0ab --- /dev/null +++ b/examples/DancingGoat/AdminComponents/UIPages/UniqueProductSkuValidationRule.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat.AdminComponents.UIPages; +using DancingGoat.Commerce; + +using Kentico.Xperience.Admin.Base.Authentication; +using Kentico.Xperience.Admin.Base.Forms; + +[assembly: RegisterFormValidationRule(UniqueProductSkuValidationRule.IDENTIFIER, typeof(UniqueProductSkuValidationRule), "Unique SKU value", "Checks whether the field does not contain a product SKU that is already being used.")] + +namespace DancingGoat.AdminComponents.UIPages; + +/// +/// Rule validates that underlying field does not contain a product SKU that is already used in another product. +/// +internal class UniqueProductSkuValidationRule : ValidationRule +{ + public const string IDENTIFIER = "DancingGoat.UniqueSkuValidationRule"; + + private readonly ProductSkuValidator productSkuValidator; + private readonly IContentItemManagerFactory contentItemManagerFactory; + private readonly IAuthenticatedUserAccessor authenticatedUserAccessor; + + + public UniqueProductSkuValidationRule(ProductSkuValidator productSkuValidator, IContentItemManagerFactory contentItemManagerFactory, + IAuthenticatedUserAccessor authenticatedUserAccessor) + { + this.productSkuValidator = productSkuValidator; + this.contentItemManagerFactory = contentItemManagerFactory; + this.authenticatedUserAccessor = authenticatedUserAccessor; + } + + + /// + /// Returns validation result if the product SKU is not used in another product; otherwise + /// + /// Value to be validated. + /// Provider of values of other form fields for contextual validation. + /// Returns the validation result. + public override async Task Validate(string value, IFormFieldValueProvider formFieldValueProvider) + { + var contentItemFormContext = FormContext as IContentItemFormContextBase; + if (contentItemFormContext == null) + { + throw new InvalidOperationException("The validation rule can only be used in a content item form context."); + } + + int contentItemId = contentItemFormContext.ItemId; + + // Try to find a colliding content item using the provided SKU code + int? collidingContentItemIdentifier = await productSkuValidator.GetCollidingContentItem(value, contentItemId); + + if (collidingContentItemIdentifier == null) + { + // The SKU code is unique, the validation passes + return ValidationResult.Success; + } + else + { + // The SKU code is already used in another product, the validation fails + var user = await authenticatedUserAccessor.Get(); + if (user == null) + { + throw new InvalidOperationException("No authenticated user was found."); + } + + var contentItemManager = contentItemManagerFactory.Create(user.UserID); + + var metadata = await contentItemManager.GetContentItemLanguageMetadata(collidingContentItemIdentifier.Value, contentItemFormContext.LanguageName); + if (metadata == null) + { + throw new InvalidOperationException($"Content item metadata with ID {contentItemId} was not found."); + } + + return new(false, $"Product SKU is already being used in the product '{metadata.DisplayName}'."); + } + } +} diff --git a/examples/DancingGoat/Commerce/Checkout/DancingGoatCheckoutController.cs b/examples/DancingGoat/Commerce/Checkout/DancingGoatCheckoutController.cs new file mode 100644 index 0000000..a47d38a --- /dev/null +++ b/examples/DancingGoat/Commerce/Checkout/DancingGoatCheckoutController.cs @@ -0,0 +1,257 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.Commerce; +using CMS.ContentEngine; +using CMS.Membership; + +using DancingGoat; +using DancingGoat.Commerce; +using DancingGoat.Helpers; +using DancingGoat.Models; +using DancingGoat.Services; + +using Kentico.Commerce.Web.Mvc; +using Kentico.Content.Web.Mvc.Routing; +using Kentico.Membership; + +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +[assembly: RegisterWebPageRoute(Checkout.CONTENT_TYPE_NAME, typeof(DancingGoatCheckoutController), WebsiteChannelNames = new[] { DancingGoatConstants.WEBSITE_CHANNEL_NAME })] + +namespace DancingGoat.Commerce; + +/// +/// Controller for managing the checkout process. +/// +public sealed class DancingGoatCheckoutController : Controller +{ + private readonly CountryStateRepository countryStateRepository; + private readonly WebPageUrlProvider webPageUrlProvider; + private readonly ICurrentShoppingCartService currentShoppingCartService; + private readonly UserManager userManager; + private readonly CustomerDataRetriever customerDataRetriever; + private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly OrderService orderService; + private readonly IStringLocalizer localizer; + private readonly ProductNameProvider productNameProvider; + private readonly ProductRepository productRepository; + + public DancingGoatCheckoutController( + CountryStateRepository countryStateRepository, + WebPageUrlProvider webPageUrlProvider, + ICurrentShoppingCartService currentShoppingCartService, + UserManager userManager, + CustomerDataRetriever customerDataRetriever, + IPreferredLanguageRetriever currentLanguageRetriever, + OrderService orderService, + IStringLocalizer localizer, + ProductNameProvider productNameProvider, + ProductRepository productRepository) + { + this.countryStateRepository = countryStateRepository; + this.webPageUrlProvider = webPageUrlProvider; + this.currentShoppingCartService = currentShoppingCartService; + this.userManager = userManager; + this.customerDataRetriever = customerDataRetriever; + this.currentLanguageRetriever = currentLanguageRetriever; + this.orderService = orderService; + this.localizer = localizer; + this.productNameProvider = productNameProvider; + this.productRepository = productRepository; + } + + + [HttpGet] + public async Task Index(CancellationToken cancellationToken) + { + return View(await GetCheckoutViewModel(CheckoutStep.CheckoutCustomer, null, null, null, cancellationToken)); + } + + + [HttpPost] + public async Task Index(CustomerViewModel customer, CustomerAddressViewModel customerAddress, CheckoutStep checkoutStep, CancellationToken cancellationToken) + { + // Validate state selection based on the selected country + int.TryParse(customerAddress.CountryId, out int countryId); + var countryStates = await countryStateRepository.GetStates(countryId, cancellationToken); + bool selectedStateValidationResult = !countryStates.Any() || !string.IsNullOrEmpty(customerAddress.StateId); + if (!selectedStateValidationResult) + { + ModelState.AddModelError($"{nameof(customerAddress)}.{nameof(CustomerAddressViewModel.StateId)}", CheckoutFormConstants.REQUIRED_FIELD_ERROR_MESSAGE); + } + + if (!ModelState.IsValid || checkoutStep == CheckoutStep.CheckoutCustomer) + { + return View(await GetCheckoutViewModel(CheckoutStep.CheckoutCustomer, customer, customerAddress, null, cancellationToken)); + } + + var shoppingCart = await currentShoppingCartService.Get(cancellationToken); + if (shoppingCart == null) + { + return View(await GetCheckoutViewModel(CheckoutStep.OrderConfirmation, customer, customerAddress, new ShoppingCartViewModel(new List(), 0), cancellationToken)); + } + + var shoppingCartViewModel = await GetShoppingCartViewModel(shoppingCart, cancellationToken); + + return View(await GetCheckoutViewModel(CheckoutStep.OrderConfirmation, customer, customerAddress, shoppingCartViewModel, cancellationToken)); + } + + + [HttpPost] + [Route("/Checkout/GetStates")] + public async Task> GetStates(int countryId, CancellationToken cancellationToken) + { + if (countryId > 0) + { + var states = await countryStateRepository.GetStates(countryId, cancellationToken); + return states.Select(x => new SelectListItem() + { + Text = x.StateDisplayName, + Value = x.StateID.ToString(), + }).ToList(); + } + return new List(); + } + + + [HttpPost] + [Route("{languageName}/OrderConfirmation/ConfirmOrder")] + public async Task ConfirmOrder(CustomerViewModel customer, CustomerAddressViewModel customerAddress, string languageName, CancellationToken cancellationToken) + { + // Add the current language to the route values in order to tell XbyK what the current language is + // since this route is not handled by the XbyK content-tree-based routing + HttpContext.Request.RouteValues.Add(WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY, languageName); + + if (!ModelState.IsValid) + { + Redirect(await webPageUrlProvider.CheckoutPageUrl(languageName, cancellationToken: cancellationToken)); + } + + var user = await GetAuthenticatedUser(); + + var shoppingCart = await currentShoppingCartService.Get(cancellationToken); + if (shoppingCart == null) + { + return Content(localizer["Order not created. The shopping cart could not be found."]); + } + + var customerDto = customer.ToCustomerDto(customerAddress); + var shoppingCartData = shoppingCart.GetShoppingCartDataModel(); + + var orderNumber = await orderService.CreateOrder(shoppingCartData, customerDto, user?.Id ?? 0, cancellationToken); + + await currentShoppingCartService.Discard(cancellationToken); + + return View(new ConfirmOrderViewModel(orderNumber)); + } + + + private async Task GetCheckoutViewModel(CheckoutStep step, CustomerViewModel customerViewModel, CustomerAddressViewModel customerAddressViewModel, ShoppingCartViewModel shoppingCartViewModel, + CancellationToken cancellationToken) + { + var user = await GetAuthenticatedUser(); + + // No model data is provided => try to retrieve data from the registered member/customer + if (user != null && customerViewModel == null) + { + // Retrieve email information for the registered member + customerViewModel = new CustomerViewModel() + { + Email = user.Email, + }; + + // The registered member already has a customer account + var customer = await customerDataRetriever.GetCustomerForMember(user.Id, cancellationToken); + if (customer != null) + { + customerViewModel.FirstName = customer.CustomerFirstName; + customerViewModel.LastName = customer.CustomerLastName; + customerViewModel.Email = customer.CustomerEmail; + customerViewModel.PhoneNumber = customer.CustomerPhone; + + var customerAddress = await customerDataRetriever.GetCustomerAddress(customer.CustomerID, cancellationToken); + if (customerAddress != null) + { + customerViewModel.Company = customerAddress.CustomerAddressCompany; + + customerAddressViewModel ??= new CustomerAddressViewModel(); + customerAddressViewModel.Line1 = customerAddress.CustomerAddressLine1; + customerAddressViewModel.Line2 = customerAddress.CustomerAddressLine2; + customerAddressViewModel.City = customerAddress.CustomerAddressCity; + customerAddressViewModel.PostalCode = customerAddress.CustomerAddressZip; + customerAddressViewModel.CountryId = customerAddress.CustomerAddressCountryID.ToString(); + customerAddressViewModel.StateId = customerAddress.CustomerAddressStateID.ToString(); + } + } + } + + customerViewModel ??= new CustomerViewModel(); + customerAddressViewModel ??= new CustomerAddressViewModel(); + + int.TryParse(customerAddressViewModel.CountryId, out var countryId); + int.TryParse(customerAddressViewModel.StateId, out var stateId); + var countries = await countryStateRepository.GetCountries(cancellationToken); + var states = await countryStateRepository.GetStates(countryId, cancellationToken); + var countriesSelectList = countries.Select(x => new SelectListItem() { Text = x.CountryDisplayName, Value = x.CountryID.ToString() }); + + customerAddressViewModel.Countries = countriesSelectList; + customerAddressViewModel.Country = countriesSelectList.FirstOrDefault(country => country.Value == countryId.ToString())?.Text; + + customerAddressViewModel.States = states.Select(x => new SelectListItem() { Text = x.StateDisplayName, Value = x.StateID.ToString() }).ToList(); + customerAddressViewModel.State = states.FirstOrDefault(state => state.StateID == stateId)?.StateDisplayName; + + return new CheckoutViewModel(step, customerViewModel, customerAddressViewModel, shoppingCartViewModel); + } + + + private async Task GetShoppingCartViewModel(ShoppingCartInfo shoppingCart, CancellationToken cancellationToken) + { + var languageName = currentLanguageRetriever.Get(); + var shoppingCartData = shoppingCart.GetShoppingCartDataModel(); + + var products = await productRepository.GetProductsByIds(shoppingCartData.Items.Select(item => item.ContentItemId), cancellationToken); + + var productPageUrls = await productRepository.GetProductPageUrls(products.Cast().Select(p => p.SystemFields.ContentItemID), cancellationToken); + + var totalPrice = CalculationService.CalculateTotalPrice(shoppingCartData, products); + + return new ShoppingCartViewModel( + shoppingCartData.Items.Select(item => + { + var product = products.FirstOrDefault(product => (product as IContentItemFieldsSource)?.SystemFields.ContentItemID == item.ContentItemId); + productPageUrls.TryGetValue(item.ContentItemId, out var pageUrl); + var productName = productNameProvider.GetProductName(product, item.VariantId); + + return product == null + ? null + : new ShoppingCartItemViewModel( + item.ContentItemId, + productName, + product.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, + pageUrl, + item.Quantity, + product.ProductFieldPrice, + item.Quantity * product.ProductFieldPrice, + item.VariantId); + }) + .Where(x => x != null) + .ToList(), + totalPrice); + } + + + /// + /// Retrieves an authenticated live site user. + /// + /// "/> + private async Task GetAuthenticatedUser() => await userManager.GetUserAsync(User); +} + +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/Countries/CountryStateRepository.cs b/examples/DancingGoat/Commerce/Countries/CountryStateRepository.cs new file mode 100644 index 0000000..4815076 --- /dev/null +++ b/examples/DancingGoat/Commerce/Countries/CountryStateRepository.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.DataEngine; +using CMS.Globalization; +using CMS.Helpers; +using CMS.Websites.Routing; + +namespace DancingGoat.Commerce; + +/// +/// Repository for managing country and state information retrieval operations. +/// +public class CountryStateRepository +{ + private readonly IWebsiteChannelContext websiteChannelContext; + private readonly IProgressiveCache cache; + private readonly ICacheDependencyBuilderFactory cacheDependencyBuilderFactory; + private readonly IInfoProvider countryInfoProvider; + private readonly IInfoProvider stateInfoProvider; + + + /// + /// Initializes a new instance of the class. + /// + /// The website channel context. + /// The cache. + /// The cache dependency builder factory. + /// The country info provider. + /// The state info provider. + public CountryStateRepository(IWebsiteChannelContext websiteChannelContext, IProgressiveCache cache, ICacheDependencyBuilderFactory cacheDependencyBuilderFactory, + IInfoProvider countryInfoProvider, IInfoProvider stateInfoProvider) + { + this.websiteChannelContext = websiteChannelContext; + this.cache = cache; + this.cacheDependencyBuilderFactory = cacheDependencyBuilderFactory; + this.countryInfoProvider = countryInfoProvider; + this.stateInfoProvider = stateInfoProvider; + } + + + /// + /// Returns a cached list of all . + /// + public async Task> GetCountries(CancellationToken cancellationToken) + { + if (websiteChannelContext.IsPreview) + { + return await GetCountriesInternal(cancellationToken); + } + + var cacheSettings = new CacheSettings(5, websiteChannelContext.WebsiteChannelName, nameof(CountryStateRepository), nameof(GetCountries)); + + return await cache.LoadAsync(async (cacheSettings) => + { + var result = await GetCountriesInternal(cancellationToken); + + if (cacheSettings.Cached = result != null && result.Any()) + { + var cacheDependencyBuilder = cacheDependencyBuilderFactory.Create(); + var cacheDependencies = cacheDependencyBuilder + .ForInfoObjects() + .All() + .Builder() + .Build(); + cacheSettings.CacheDependency = cacheDependencies; + } + return result; + }, cacheSettings); + } + + + private async Task> GetCountriesInternal(CancellationToken cancellationToken) + { + return await countryInfoProvider.Get().GetEnumerableTypedResultAsync(cancellationToken: cancellationToken); + } + + + /// + /// Returns a cached list of all for the given country. + /// + public async Task> GetStates(int countryId, CancellationToken cancellationToken) + { + if (websiteChannelContext.IsPreview) + { + return await GetStatesInternal(countryId, cancellationToken); + } + + var cacheSettings = new CacheSettings(5, websiteChannelContext.WebsiteChannelName, nameof(CountryStateRepository), nameof(GetStates), countryId); + + return await cache.LoadAsync(async (cacheSettings) => + { + var result = await GetStatesInternal(countryId, cancellationToken); + + if (cacheSettings.Cached = result != null && result.Any()) + { + var cacheDependencyBuilder = cacheDependencyBuilderFactory.Create(); + var cacheDependencies = cacheDependencyBuilder + .ForInfoObjects() + .All() + .Builder() + .Build(); + cacheSettings.CacheDependency = cacheDependencies; + } + + return result; + }, cacheSettings); + } + + + private async Task> GetStatesInternal(int countryId, CancellationToken cancellationToken) + { + return await stateInfoProvider.Get() + .WhereEquals(nameof(StateInfo.CountryID), countryId) + .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken); + } +} diff --git a/examples/DancingGoat/Commerce/EventHandlers/ContentItemEventHandlers.cs b/examples/DancingGoat/Commerce/EventHandlers/ContentItemEventHandlers.cs new file mode 100644 index 0000000..59ebede --- /dev/null +++ b/examples/DancingGoat/Commerce/EventHandlers/ContentItemEventHandlers.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce; + +/// +/// Handles events related to content items. +/// +internal sealed class ContentItemEventHandlers +{ + private readonly ProductSkuValidator productSkuValidator; + + + /// + /// Initializes a new instance of the class. + /// + public ContentItemEventHandlers(ProductSkuValidator productSkuValidator) + { + this.productSkuValidator = productSkuValidator; + } + + + public void Initialize() + { + ContentItemEvents.Create.Before += (sender, args) => ValidateUniqueSKU(args.ContentItemData, args.ID).GetAwaiter().GetResult(); + ContentItemEvents.UpdateDraft.Before += (sender, args) => ValidateUniqueSKU(args.ContentItemData, args.ID).GetAwaiter().GetResult(); + } + + + /// + /// Validates that the SKU code is unique for the given content item. + /// + /// The content item data to validate. + /// The ID of the content item being created or updated. + private async Task ValidateUniqueSKU(ContentItemData contentItemData, int? contentItemId) + { + if (contentItemData.TryGetValue(nameof(IProductSKU.ProductSKUCode), out var skuCode)) + { + int? duplicatedContentItemIdentifier = await productSkuValidator.GetCollidingContentItem(skuCode, contentItemId); + + if (duplicatedContentItemIdentifier != null) + { + throw new InvalidOperationException($"The SKU code '{skuCode}' is already used by the content item '{duplicatedContentItemIdentifier}'."); + } + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/CoffeeParametersExtractor.cs b/examples/DancingGoat/Commerce/Extractors/CoffeeParametersExtractor.cs new file mode 100644 index 0000000..a0ece65 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/CoffeeParametersExtractor.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + public class CoffeeParametersExtractor : IProductTypeParametersExtractor + { + private readonly ITaxonomyRetriever taxonomyRetriever; + + public CoffeeParametersExtractor(ITaxonomyRetriever taxonomyRetriever) + { + this.taxonomyRetriever = taxonomyRetriever; + } + + + /// + public async Task ExtractParameter(IDictionary parameters, T product, string languageName, CancellationToken cancellationToken) + { + if (product is ProductCoffee coffee) + { + var identifiers = coffee.CoffeeProcessing.Select(x => x.Identifier) + .Union(coffee.CoffeeTastes.Select(x => x.Identifier)); + var taxonomies = await taxonomyRetriever.RetrieveTags(identifiers, languageName, cancellationToken); + + foreach (var taxonomy in taxonomies) + { + var key = coffee.CoffeeProcessing.Any(x => x.Identifier == taxonomy.Identifier) + ? "Processing" + : "Taste"; + if (parameters.TryGetValue(key, out string value)) + { + parameters[key] = $"{value}, {taxonomy.Name}"; + } + else + { + parameters.Add(key, taxonomy.Name); + } + } + } + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/GrinderParametersExtractor.cs b/examples/DancingGoat/Commerce/Extractors/GrinderParametersExtractor.cs new file mode 100644 index 0000000..6bb0ca6 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/GrinderParametersExtractor.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + public class GrinderParametersExtractor : IProductTypeParametersExtractor + { + /// + public Task ExtractParameter(IDictionary parameters, T product, string _, CancellationToken cancellationToken) + { + if (product is ProductGrinder grinder) + { + parameters.Add("Type", grinder.GrinderType); + + if (grinder.GrinderType != "Manual") + { + parameters.Add("Power", $"{grinder.GrinderPower:0} Watt"); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/IProductTypeParametersExtractor.cs b/examples/DancingGoat/Commerce/Extractors/IProductTypeParametersExtractor.cs new file mode 100644 index 0000000..6c218e9 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/IProductTypeParametersExtractor.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace DancingGoat.Commerce +{ + /// + /// Extractor of product parameters based on the product type. + /// + public interface IProductTypeParametersExtractor + { + /// + /// Extract product-specific parameters of a product based on it's type and updates parameters dictionary. + /// + /// Type of the product. + /// Dictionary containing parameters of the product that will be updated. + /// Product to get parameters from. + /// Language name to use. + /// Cancellation token. + Task ExtractParameter(IDictionary parameters, T product, string languageName, CancellationToken cancellationToken); + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/IProductTypeVariantsExtractor.cs b/examples/DancingGoat/Commerce/Extractors/IProductTypeVariantsExtractor.cs new file mode 100644 index 0000000..74fc9ef --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/IProductTypeVariantsExtractor.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace DancingGoat.Commerce +{ + /// + /// Extractor of product variants based on the product type. + /// + public interface IProductTypeVariantsExtractor + { + /// + /// Extract product-specific variants value of a product based on it's type and update variants dictionary. + /// + /// Type of the product. + /// Product to get variants from. + IDictionary ExtractVariantsValue(T product); + + + /// + /// Extract product-specific SKU code of variants of a product based on it's type and update variants dictionary. + /// + /// Type of the product. + /// Product to get SKU code variants from. + IDictionary ExtractVariantsSKUCode(T product); + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/ProductManufacturerExtractor.cs b/examples/DancingGoat/Commerce/Extractors/ProductManufacturerExtractor.cs new file mode 100644 index 0000000..fd47734 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/ProductManufacturerExtractor.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + internal class ProductManufacturerExtractor : IProductTypeParametersExtractor + { + private readonly ITaxonomyRetriever taxonomyRetriever; + + + public ProductManufacturerExtractor(ITaxonomyRetriever taxonomyRetriever) + { + this.taxonomyRetriever = taxonomyRetriever; + } + + + /// + public async Task ExtractParameter(IDictionary parameters, T product, string languageName, CancellationToken cancellationToken) + { + if (product is IProductManufacturer productManufacturer && productManufacturer.ProductManufacturerTag != null) + { + var manufacturerTags = await taxonomyRetriever.RetrieveTags(productManufacturer.ProductManufacturerTag.Select(x => x.Identifier), languageName, cancellationToken); + if (manufacturerTags.Any()) + { + parameters.Add("Manufacturer", string.Join(", ", manufacturerTags.Select(x => x.Title))); + } + } + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/ProductParametersExtractor.cs b/examples/DancingGoat/Commerce/Extractors/ProductParametersExtractor.cs new file mode 100644 index 0000000..68653a9 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/ProductParametersExtractor.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + /// Extractor of product-specific parameters. + /// + public sealed class ProductParametersExtractor + { + private readonly IEnumerable parametersExtractors; + + + public ProductParametersExtractor(IEnumerable parametersExtractors) + { + this.parametersExtractors = parametersExtractors; + } + + + /// + /// Extract product parameters and update the dictionary of parameters. + /// + /// Product to process. + /// Language name used. + /// Cancellation token. + /// Dictionary containing product parameters. + public async Task> ExtractParameters(IProductFields product, string languageName, CancellationToken cancellationToken) + { + var parameters = new Dictionary(); + + foreach (var item in parametersExtractors) + { + await item.ExtractParameter(parameters, product, languageName, cancellationToken); + } + + return parameters; + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeParametersExtractor.cs b/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeParametersExtractor.cs new file mode 100644 index 0000000..09ab499 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeParametersExtractor.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + internal class ProductTemplateAlphaSizeParametersExtractor : IProductTypeParametersExtractor + { + /// + public Task ExtractParameter(IDictionary parameters, T product, string _, CancellationToken cancellationToken) + { + if (product is ProductTemplateAlphaSize productTemplateAlphaSize) + { + var alphaSizes = productTemplateAlphaSize.ProductVariants.Select(x => x.ProductOptionAlphaSize).Distinct(); + + parameters.Add("Sizes", string.Join(", ", alphaSizes)); + } + + return Task.CompletedTask; + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeVariantsExtractor.cs b/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeVariantsExtractor.cs new file mode 100644 index 0000000..10c393e --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/ProductTemplateAlphaSizeVariantsExtractor.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + internal class ProductTemplateAlphaSizeVariantsExtractor : IProductTypeVariantsExtractor + { + /// + public IDictionary ExtractVariantsValue(T product) + { + if (product is ProductTemplateAlphaSize productTemplateAlphaSize) + { + return productTemplateAlphaSize.ProductVariants.ToDictionary(x => x.SystemFields.ContentItemID, x => x.ProductOptionAlphaSize); + } + return null; + } + + + /// + public IDictionary ExtractVariantsSKUCode(T product) + { + if (product is ProductTemplateAlphaSize productTemplateAlphaSize) + { + return productTemplateAlphaSize.ProductVariants.ToDictionary(x => x.SystemFields.ContentItemID, x => x.ProductSKUCode); + } + return null; + } + } +} diff --git a/examples/DancingGoat/Commerce/Extractors/ProductVariantsExtractor.cs b/examples/DancingGoat/Commerce/Extractors/ProductVariantsExtractor.cs new file mode 100644 index 0000000..a67c847 --- /dev/null +++ b/examples/DancingGoat/Commerce/Extractors/ProductVariantsExtractor.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce +{ + /// + /// Extractor of product-specific variants. + /// + public sealed class ProductVariantsExtractor + { + private readonly IEnumerable parametersExtractors; + + + public ProductVariantsExtractor(IEnumerable parametersExtractors) + { + this.parametersExtractors = parametersExtractors; + } + + + /// + /// Extract product variants and update the dictionary of variants. + /// + /// Product to process. + /// Dictionary containing product variants. + public IDictionary ExtractVariantsValue(IProductFields product) + { + var result = new Dictionary(); + + foreach (var item in parametersExtractors) + { + var variants = item.ExtractVariantsValue(product); + if (variants != null) + { + foreach (var variant in variants) + { + result.Add(variant.Key, variant.Value); + } + } + } + + return result; + } + + + /// + /// Extract product variants SKU code and update the dictionary of variants. + /// + /// Product to process. + /// Dictionary containing product variants SKU code. + public IDictionary ExtractVariantsSKUCode(IProductFields product) + { + var result = new Dictionary(); + + foreach (var item in parametersExtractors) + { + var variants = item.ExtractVariantsSKUCode(product); + if (variants != null) + { + foreach (var variant in variants) + { + result.Add(variant.Key, variant.Value); + } + } + } + + return result; + } + } +} diff --git a/examples/DancingGoat/Commerce/PriceFormatter.cs b/examples/DancingGoat/Commerce/PriceFormatter.cs new file mode 100644 index 0000000..a47d903 --- /dev/null +++ b/examples/DancingGoat/Commerce/PriceFormatter.cs @@ -0,0 +1,25 @@ +using System.Globalization; + +using CMS; +using CMS.Commerce; + +using DancingGoat.Commerce; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +[assembly: RegisterImplementation(typeof(IPriceFormatter), typeof(PriceFormatter))] + +namespace DancingGoat.Commerce; + +/// +/// Represents the Dancing goat price formatter. +/// +internal sealed class PriceFormatter : IPriceFormatter +{ + public string Format(decimal price, PriceFormatConfiguration configuration) + { + const string CULTURE_CODE_EN_US = "en-US"; + + return price.ToString("C2", CultureInfo.CreateSpecificCulture(CULTURE_CODE_EN_US)); + } +} +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/Services/CalculationService.cs b/examples/DancingGoat/Commerce/Services/CalculationService.cs new file mode 100644 index 0000000..6e046b2 --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/CalculationService.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; + +using CMS.ContentEngine; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce; + +internal sealed class CalculationService +{ + /// + /// Calculate total price of all items within shopping cart with specified products. + /// + public static decimal CalculateTotalPrice(ShoppingCartDataModel shoppingCartDataModel, IEnumerable products) + { + return shoppingCartDataModel.Items + .Sum(item => CalculateItemPrice( + item.Quantity, + products.First(product => + (product as IContentItemFieldsSource).SystemFields.ContentItemID == item.ContentItemId + ).ProductFieldPrice + )); + } + + + /// + /// Calculate price of all units of a product. + /// + public static decimal CalculateItemPrice(int quantity, decimal unitPrice) + { + return quantity * unitPrice; + } +} diff --git a/examples/DancingGoat/Commerce/Services/CustomerDataRetriever.cs b/examples/DancingGoat/Commerce/Services/CustomerDataRetriever.cs new file mode 100644 index 0000000..0c5087e --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/CustomerDataRetriever.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.Commerce; +using CMS.DataEngine; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +namespace DancingGoat.Commerce; + +/// +/// Service for customer data retrieval. +/// +public sealed class CustomerDataRetriever +{ + private readonly IInfoProvider customerInfoProvider; + private readonly IInfoProvider customerAddressInfoProvider; + + + public CustomerDataRetriever(IInfoProvider customerInfoProvider, IInfoProvider customerAddressInfoProvider) + { + this.customerInfoProvider = customerInfoProvider; + this.customerAddressInfoProvider = customerAddressInfoProvider; + } + + + /// + /// Returns a customer object for the given member ID. + /// + public async Task GetCustomerForMember(int memberId, CancellationToken cancellationToken) + { + return (await customerInfoProvider + .Get() + .WhereEquals(nameof(CustomerInfo.CustomerMemberID), memberId) + .TopN(1) + .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken)) + .FirstOrDefault(); + } + + + /// + /// Returns a customer address object for the given customer ID. + /// + public async Task GetCustomerAddress(int customerId, CancellationToken cancellationToken) + { + return (await customerAddressInfoProvider + .Get() + .WhereEquals(nameof(CustomerAddressInfo.CustomerAddressCustomerID), customerId) + .TopN(1) + .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken)) + .FirstOrDefault(); + } +} +#pragma warning restore KXE0002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/Services/OrderNumberGenerator.cs b/examples/DancingGoat/Commerce/Services/OrderNumberGenerator.cs new file mode 100644 index 0000000..9acaf03 --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/OrderNumberGenerator.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Text.RegularExpressions; + +using CMS.Commerce; +using CMS.DataEngine; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +namespace DancingGoat.Commerce; + +/// +/// Service responsible for generating unique order numbers. +/// +public sealed partial class OrderNumberGenerator +{ + private readonly IInfoProvider orderInfoProvider; + + + public OrderNumberGenerator(IInfoProvider orderInfoProvider) + { + this.orderInfoProvider = orderInfoProvider; + } + + + /// + /// Generates a new unique order number in the format "YYYY-N", where N is the sequential number for the year. + /// + /// Cancellation token for async operation. + /// A task that represents the asynchronous operation. The task result contains the generated order number. + public async Task GenerateOrderNumber(CancellationToken cancellationToken) + { + var actualYear = DateTime.Now.Year; + var beginningOfTheYear = new DateTime(actualYear, 1, 1); + + var lastOrderNumber = await orderInfoProvider.Get() + .WhereGreaterOrEquals(nameof(OrderInfo.OrderCreatedWhen), beginningOfTheYear) + .OrderByDescending(nameof(OrderInfo.OrderCreatedWhen)) + .TopN(1) + .Column(nameof(OrderInfo.OrderNumber)) + .GetScalarResultAsync(cancellationToken: cancellationToken); + + var orderSequenceNumber = GetNextOrderSequenceNumber(lastOrderNumber); + + return FormatOrderNumber(actualYear, orderSequenceNumber); + } + + + /// + /// Formats the order number using the given year and sequence number. + /// + /// The year to include in the order number. + /// The sequential number for the given year. + /// The formatted order number string. + private static string FormatOrderNumber(int year, int orderSequenceNumber) => $"{year}-{orderSequenceNumber}"; + + + /// + /// Parses the last order number and calculates the next sequential number. + /// + /// The last generated order number in the format "YYYY-N". + /// The next sequence number for the current year. + private static int GetNextOrderSequenceNumber(string orderNumber) + { + var match = LastNumberRegex().Match(orderNumber ?? string.Empty); + var parsedSequenceNumber = match.Success && int.TryParse(match.Groups[1].Value, out var parsedNumber) + ? parsedNumber + : 0; + + return parsedSequenceNumber + 1; + } + + + /// + /// Regex to extract the numeric sequence from the end of the order number (e.g., "2025-12" → 12). + /// + /// The compiled regex instance. + [GeneratedRegex(@"-(\d+)$")] + private static partial Regex LastNumberRegex(); +} +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/Services/OrderService.cs b/examples/DancingGoat/Commerce/Services/OrderService.cs new file mode 100644 index 0000000..38ec75e --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/OrderService.cs @@ -0,0 +1,212 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.Commerce; +using CMS.ContentEngine; +using CMS.DataEngine; + +using DancingGoat.Models; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +namespace DancingGoat.Commerce; + +/// +/// Service for managing orders. +/// +public sealed class OrderService +{ + private readonly ProductVariantsExtractor productVariantsExtractor; + private readonly ProductNameProvider productNameProvider; + private readonly IInfoProvider orderInfoProvider; + private readonly IInfoProvider orderItemInfoProvider; + private readonly IInfoProvider orderAddressInfoProvider; + private readonly IInfoProvider customerInfoProvider; + private readonly IInfoProvider customerAddressInfoProvider; + private readonly CustomerDataRetriever customerDataRetriever; + private readonly OrderNumberGenerator orderNumberGenerator; + private readonly ProductRepository productRepository; + private readonly IInfoProvider orderStatusInfoProvider; + private readonly IOrderNotificationService orderNotificationService; + + + public OrderService( + ProductVariantsExtractor productVariantsExtractor, + ProductNameProvider productNameProvider, + IInfoProvider orderInfoProvider, + IInfoProvider orderItemInfoProvider, + IInfoProvider orderAddressInfoProvider, + IInfoProvider customerInfoProvider, + IInfoProvider customerAddressInfoProvider, + CustomerDataRetriever customerDataRetriever, + OrderNumberGenerator orderNumberGenerator, + ProductRepository productRepository, + IInfoProvider orderStatusInfoProvider, + IOrderNotificationService orderNotificationService) + { + this.productVariantsExtractor = productVariantsExtractor; + this.productNameProvider = productNameProvider; + this.orderInfoProvider = orderInfoProvider; + this.orderItemInfoProvider = orderItemInfoProvider; + this.orderAddressInfoProvider = orderAddressInfoProvider; + this.customerInfoProvider = customerInfoProvider; + this.customerAddressInfoProvider = customerAddressInfoProvider; + this.customerDataRetriever = customerDataRetriever; + this.orderNumberGenerator = orderNumberGenerator; + this.productRepository = productRepository; + this.orderStatusInfoProvider = orderStatusInfoProvider; + this.orderNotificationService = orderNotificationService; + } + + + /// + /// Creates an order based on the provided shopping cart and customer information. + /// + /// Returns order number of newly create order. + public async Task CreateOrder(ShoppingCartDataModel shoppingCartData, CustomerDto customerDto, int memberId, CancellationToken cancellationToken) + { + + var products = await productRepository.GetProductsByIds(shoppingCartData.Items.Select(item => item.ContentItemId), cancellationToken); + + var totalPrice = CalculationService.CalculateTotalPrice(shoppingCartData, products); + + using (var scope = new CMSTransactionScope()) + { + int customerId = await UpsertCustomer(customerDto, memberId, cancellationToken); + + var orderNumber = await orderNumberGenerator.GenerateOrderNumber(cancellationToken); + var orderStatusId = await GetInitialOrderStatusId(cancellationToken); + + var order = new OrderInfo() + { + OrderCreatedWhen = DateTime.Now, + OrderNumber = orderNumber, + OrderOrderStatusID = orderStatusId, + OrderTotalPrice = totalPrice, + OrderTotalTax = 0, + OrderTotalShipping = 0, + OrderGrandTotal = totalPrice, + OrderCustomerID = customerId + }; + await orderInfoProvider.SetAsync(order); + + var orderAddress = new OrderAddressInfo() + { + OrderAddressFirstName = customerDto.FirstName, + OrderAddressLastName = customerDto.LastName, + OrderAddressCompany = customerDto.Company, + OrderAddressPhone = customerDto.PhoneNumber, + OrderAddressEmail = customerDto.Email, + OrderAddressCity = customerDto.AddressCity, + OrderAddressLine1 = customerDto.AddressLine1, + OrderAddressLine2 = customerDto.AddressLine2, + OrderAddressZip = customerDto.AddressPostalCode, + OrderAddressCountryID = customerDto.AddressCountryId, + OrderAddressStateID = customerDto.AddressStateId, + OrderAddressOrderID = order.OrderID, + OrderAddressType = "Billing", + }; + await orderAddressInfoProvider.SetAsync(orderAddress); + + foreach (var item in shoppingCartData.Items) + { + var product = products.First(product => (product as IContentItemFieldsSource).SystemFields.ContentItemID == item.ContentItemId); + var variantSKUs = product == null ? null : productVariantsExtractor.ExtractVariantsSKUCode(product); + var variantSKU = variantSKUs == null || !item.VariantId.HasValue ? null : variantSKUs[item.VariantId.Value]; + var productName = productNameProvider.GetProductName(product, item.VariantId); + + var unitPrice = product.ProductFieldPrice; + var orderItem = new OrderItemInfo() + { + OrderItemOrderID = order.OrderID, + OrderItemUnitCount = item.Quantity, + OrderItemUnitPrice = unitPrice, + OrderItemTotalPrice = CalculationService.CalculateItemPrice(item.Quantity, unitPrice), + OrderItemSKU = variantSKU ?? (product as IProductSKU).ProductSKUCode, + OrderItemName = productName + }; + await orderItemInfoProvider.SetAsync(orderItem); + } + + scope.Commit(); + + await orderNotificationService.SendNotification(order.OrderID, cancellationToken); + + return orderNumber; + } + } + + + /// + /// Updates or creates a customer and their address based on the provided data. + /// + private async Task UpsertCustomer(CustomerDto customerDto, int memberId, CancellationToken cancellation) + { + CustomerInfo customer = null; + + if (memberId > 0) + { + customer = await customerDataRetriever.GetCustomerForMember(memberId, cancellation); + } + + if (customer == null) + { + // Create a new customer if it doesn't exist for the member yet or if the member is not authenticated + customer = new CustomerInfo() + { + CustomerCreatedWhen = DateTime.Now, + CustomerMemberID = memberId + }; + } + + // Update the customer with the data from the checkout form + customer.CustomerFirstName = customerDto.FirstName; + customer.CustomerLastName = customerDto.LastName; + customer.CustomerEmail = customerDto.Email; + customer.CustomerPhone = customerDto.PhoneNumber; + + await customerInfoProvider.SetAsync(customer); + + // Do not cancel the request while a write operation is already in process + var customerAddress = await customerDataRetriever.GetCustomerAddress(customer.CustomerID, CancellationToken.None); + if (customerAddress == null) + { + customerAddress = new CustomerAddressInfo() + { + CustomerAddressCustomerID = customer.CustomerID + }; + } + + // Update the customer address with the data from the checkout form + // (Dancing Goat sample operates only with a single customer address) + customerAddress.CustomerAddressFirstName = customerDto.FirstName; + customerAddress.CustomerAddressLastName = customerDto.LastName; + customerAddress.CustomerAddressCompany = customerDto.Company; + customerAddress.CustomerAddressEmail = customerDto.Email; + customerAddress.CustomerAddressPhone = customerDto.PhoneNumber; + customerAddress.CustomerAddressLine1 = customerDto.AddressLine1; + customerAddress.CustomerAddressLine2 = customerDto.AddressLine2; + customerAddress.CustomerAddressCity = customerDto.AddressCity; + customerAddress.CustomerAddressZip = customerDto.AddressPostalCode; + customerAddress.CustomerAddressCountryID = customerDto.AddressCountryId; + customerAddress.CustomerAddressStateID = customerDto.AddressStateId; + + await customerAddressInfoProvider.SetAsync(customerAddress); + + return customer.CustomerID; + } + + + /// + /// Get initial order status ID by sorting order status by OrderStatusOrder and taking the first one. + /// + private async Task GetInitialOrderStatusId(CancellationToken cancellationToken) => + await orderStatusInfoProvider + .Get() + .OrderByAscending(nameof(OrderStatusInfo.OrderStatusOrder)) + .TopN(1) + .Column(nameof(OrderStatusInfo.OrderStatusID)) + .GetScalarResultAsync(cancellationToken: cancellationToken); +} +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/Services/ProductNameProvider.cs b/examples/DancingGoat/Commerce/Services/ProductNameProvider.cs new file mode 100644 index 0000000..631f8a0 --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/ProductNameProvider.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce; + +public sealed class ProductNameProvider +{ + private readonly ProductVariantsExtractor productVariantsExtractor; + + + public ProductNameProvider(ProductVariantsExtractor productVariantsExtractor) + { + this.productVariantsExtractor = productVariantsExtractor; + } + + + public string GetProductName(IProductFields product, int? variantId = null) + { + var variantValues = product == null ? null : productVariantsExtractor.ExtractVariantsValue(product); + return FormatProductName(product?.ProductFieldName, variantValues, variantId); + } + + + private static string FormatProductName(string productName, IDictionary variants, int? variantId) + { + return variants != null && variantId != null && variants.TryGetValue(variantId.Value, out var variantValue) + ? $"{productName} - {variantValue}" + : productName; + } +} diff --git a/examples/DancingGoat/Commerce/Services/ProductSkuValidator.cs b/examples/DancingGoat/Commerce/Services/ProductSkuValidator.cs new file mode 100644 index 0000000..a3bb975 --- /dev/null +++ b/examples/DancingGoat/Commerce/Services/ProductSkuValidator.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat.Models; + +namespace DancingGoat.Commerce; + +/// +/// Provides functionality to validate product SKU codes against existing content items. +/// This class ensures that SKU codes are unique across published and draft versions of content items. +/// +internal sealed class ProductSkuValidator +{ + private readonly IContentQueryExecutor executor; + + + public ProductSkuValidator(IContentQueryExecutor executor) + { + this.executor = executor; + } + + + /// + /// Checks if the provided SKU code is already used by another content item. + /// + /// The SKU code to check. + /// The ID of the content item to exclude from the check. This is used when updating an existing content item. + /// The identifier of the duplicate content item or null if no duplicates were found. + public async Task GetCollidingContentItem(string skuCode, int? contentItemId) + { + var queryBuilder = new ContentItemQueryBuilder() + .ForContentTypes(ct => ct.OfReusableSchema(IProductSKU.REUSABLE_FIELD_SCHEMA_NAME)) + .Parameters(p => + p.Where(w => w.WhereEquals(nameof(IProductSKU.ProductSKUCode), skuCode)) + ); + + // Exclude the current content item from the query if it is provided + if (contentItemId != null) + { + queryBuilder.Parameters(p => + p.Where(w => w.WhereNotEquals(nameof(IContentItemFieldsSource.SystemFields.ContentItemID), contentItemId)) + ); + } + + // Searches for product SKUs in the published versions of products + var publishedDuplicateProducts = await executor.GetResult(queryBuilder, + rowData => rowData.ContentItemID, + new ContentQueryExecutionOptions { ForPreview = false }); + + // Searches for product SKUs in the draft versions of products + queryBuilder.Parameters(p => + p.Where(w => w.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemCommonDataVersionStatus), [(int)VersionStatus.InitialDraft, (int)VersionStatus.Draft])) + ); + + var draftDuplicate = await executor.GetResult(queryBuilder, + rowData => rowData.ContentItemID, + new ContentQueryExecutionOptions { ForPreview = true }); + + return publishedDuplicateProducts.FirstOrDefault() ?? draftDuplicate.FirstOrDefault(); + } +} diff --git a/examples/DancingGoat/Commerce/ShoppingCart/DancingGoatShoppingCartController.cs b/examples/DancingGoat/Commerce/ShoppingCart/DancingGoatShoppingCartController.cs new file mode 100644 index 0000000..9aea752 --- /dev/null +++ b/examples/DancingGoat/Commerce/ShoppingCart/DancingGoatShoppingCartController.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.Commerce; +using CMS.ContentEngine; + +using DancingGoat; +using DancingGoat.Commerce; +using DancingGoat.Helpers; +using DancingGoat.Models; +using DancingGoat.Services; + +using Kentico.Commerce.Web.Mvc; +using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Mvc; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +[assembly: RegisterWebPageRoute(ShoppingCart.CONTENT_TYPE_NAME, typeof(DancingGoatShoppingCartController), WebsiteChannelNames = new[] { DancingGoatConstants.WEBSITE_CHANNEL_NAME })] + +namespace DancingGoat.Commerce; + +/// +/// Controller for managing the shopping cart. +/// +public sealed class DancingGoatShoppingCartController : Controller +{ + private readonly ICurrentShoppingCartService currentShoppingCartService; + private readonly ProductVariantsExtractor productVariantsExtractor; + private readonly WebPageUrlProvider webPageUrlProvider; + private readonly ProductRepository productRepository; + + public DancingGoatShoppingCartController( + ICurrentShoppingCartService currentShoppingCartService, + ProductVariantsExtractor productVariantsExtractor, + WebPageUrlProvider webPageUrlProvider, + ProductRepository productRepository) + { + this.currentShoppingCartService = currentShoppingCartService; + this.productVariantsExtractor = productVariantsExtractor; + this.webPageUrlProvider = webPageUrlProvider; + this.productRepository = productRepository; + } + + + public async Task Index(CancellationToken cancellationToken) + { + var shoppingCart = await currentShoppingCartService.Get(cancellationToken); + if (shoppingCart == null) + { + return View(new ShoppingCartViewModel(new List(), 0)); + } + + var shoppingCartData = shoppingCart.GetShoppingCartDataModel(); + + var products = await productRepository.GetProductsByIds(shoppingCartData.Items.Select(item => item.ContentItemId), cancellationToken); + + var productPageUrls = await productRepository.GetProductPageUrls(products.Cast().Select(p => p.SystemFields.ContentItemID), cancellationToken); + + var totalPrice = CalculationService.CalculateTotalPrice(shoppingCartData, products); + + return View(new ShoppingCartViewModel( + shoppingCartData.Items.Select(item => + { + var product = products.FirstOrDefault(product => (product as IContentItemFieldsSource)?.SystemFields.ContentItemID == item.ContentItemId); + var variantValues = product == null ? null : productVariantsExtractor.ExtractVariantsValue(product); + productPageUrls.TryGetValue(item.ContentItemId, out var pageUrl); + + return product == null + ? null + : new ShoppingCartItemViewModel( + item.ContentItemId, + FormatProductName(product.ProductFieldName, variantValues, item.VariantId), + product.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, + pageUrl, + item.Quantity, + product.ProductFieldPrice, + item.Quantity * product.ProductFieldPrice, + item.VariantId); + }) + .Where(x => x != null) + .ToList(), + totalPrice)); + } + + + [HttpPost] + [Route("/ShoppingCart/HandleAddRemove")] + public async Task HandleAddRemove(int contentItemId, int quantity, int? variantId, string action, string languageName) + { + if (string.Equals(action, "Remove", StringComparison.OrdinalIgnoreCase)) + { + quantity *= -1; + } + else if (action == "RemoveAll") + { + quantity = 0; + } + + var shoppingCart = await GetCurrentShoppingCart(); + + UpdateQuantity(shoppingCart, contentItemId, quantity, variantId, setAbsoluteValue: new[] { "RemoveAll", "Update" }.Contains(action)); + + shoppingCart.Update(); + + return Redirect(await webPageUrlProvider.ShoppingCartPageUrl(languageName)); + } + + + [HttpPost] + [Route("/ShoppingCart/Add")] + public async Task Add(int contentItemId, int quantity, int? variantId, string languageName) + { + var shoppingCart = await GetCurrentShoppingCart(); + + UpdateQuantity(shoppingCart, contentItemId, quantity, variantId); + + shoppingCart.Update(); + + return Redirect(await webPageUrlProvider.ShoppingCartPageUrl(languageName)); + } + + + private static string FormatProductName(string productName, IDictionary variants, int? variantId) + { + return variants != null && variantId != null && variants.TryGetValue(variantId.Value, out string variantValue) + ? $"{productName} - {variantValue}" + : productName; + } + + + /// + /// Updates the quantity of the product in the shopping cart. + /// + private static void UpdateQuantity(ShoppingCartInfo shoppingCart, int contentItemId, int quantity, int? variantId, bool setAbsoluteValue = false) + { + var shoppingCartData = shoppingCart.GetShoppingCartDataModel(); + + var productItem = shoppingCartData.Items.FirstOrDefault(x => x.ContentItemId == contentItemId && x.VariantId == variantId); + if (productItem != null) + { + productItem.Quantity = setAbsoluteValue ? quantity : Math.Max(0, productItem.Quantity + quantity); + if (productItem.Quantity == 0) + { + shoppingCartData.Items.Remove(productItem); + } + } + else if (quantity > 0) + { + shoppingCartData.Items.Add(new ShoppingCartDataItem { ContentItemId = contentItemId, Quantity = quantity, VariantId = variantId }); + } + + shoppingCart.StoreShoppingCartDataModel(shoppingCartData); + } + + + /// + /// Gets the current shopping cart or creates a new one if it does not exist. + /// + private async Task GetCurrentShoppingCart() + { + var shoppingCart = await currentShoppingCartService.Get(); + + shoppingCart ??= await currentShoppingCartService.Create(null); + + return shoppingCart; + } +} +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataItem.cs b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataItem.cs new file mode 100644 index 0000000..bbd6ff9 --- /dev/null +++ b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataItem.cs @@ -0,0 +1,19 @@ +namespace DancingGoat.Commerce; + +public sealed class ShoppingCartDataItem +{ + /// + /// Identifier of the content item representing a product. + /// + public int ContentItemId { get; set; } + + /// + /// Quantity of the item in shopping cart. + /// + public int Quantity { get; set; } + + /// + /// Identifier of the variant representing specific variant of a product. + /// + public int? VariantId { get; set; } +} diff --git a/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModel.cs b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModel.cs new file mode 100644 index 0000000..0308d49 --- /dev/null +++ b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace DancingGoat.Commerce; + +public sealed class ShoppingCartDataModel +{ + /// + /// Items inside the shopping cart. + /// + public ICollection Items { get; init; } = new List(); +} diff --git a/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModelExtensions.cs b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModelExtensions.cs new file mode 100644 index 0000000..9d09eec --- /dev/null +++ b/examples/DancingGoat/Commerce/ShoppingCart/ShoppingCartDataModelExtensions.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +using CMS.Commerce; + +using DancingGoat.Commerce; + +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +namespace DancingGoat.Helpers; + +internal static class ShoppingCartDataModelExtensions +{ + /// + /// Deserializes the shopping cart data model from the shopping cart object. + /// + public static ShoppingCartDataModel GetShoppingCartDataModel(this ShoppingCartInfo shoppingCart) + { + return (string.IsNullOrEmpty(shoppingCart?.ShoppingCartData) ? null : JsonSerializer.Deserialize(shoppingCart.ShoppingCartData)) + ?? new ShoppingCartDataModel(); + } + + + /// + /// Serializes the shopping cart data model in the shopping cart object. + /// + public static void StoreShoppingCartDataModel(this ShoppingCartInfo shoppingCart, ShoppingCartDataModel shoppingCartData) + { + shoppingCart.ShoppingCartData = JsonSerializer.Serialize(shoppingCartData); + } +} +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs b/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs index f5ab73f..fca407c 100644 --- a/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs +++ b/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; -using Kentico.PageBuilder.Web.Mvc; - using DancingGoat.Widgets; +using Kentico.PageBuilder.Web.Mvc; + namespace DancingGoat.Sections { /// diff --git a/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs index 3675cda..f48396d 100644 --- a/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs @@ -6,7 +6,7 @@ using DancingGoat.Models; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; @@ -18,52 +18,56 @@ namespace DancingGoat.ViewComponents /// public class ArticlesViewComponent : ViewComponent { - private readonly ArticlePageRepository articlePageRepository; - private readonly ArticlesSectionRepository articlesSectionRepository; - private readonly IWebPageUrlRetriever urlRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; private const int ARTICLES_PER_VIEW = 5; public ArticlesViewComponent( - ArticlePageRepository articlePageRepository, - ArticlesSectionRepository articlesSectionRepository, - IWebPageUrlRetriever urlRetriever, - IPreferredLanguageRetriever currentLanguageRetriever) + IContentRetriever contentRetriever) { - this.articlePageRepository = articlePageRepository; - this.articlesSectionRepository = articlesSectionRepository; - this.urlRetriever = urlRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync(WebPageRelatedItem articlesSectionItem) { - var languageName = currentLanguageRetriever.Get(); + var articlesSection = (await contentRetriever.RetrievePagesByGuids( + [articlesSectionItem.WebPageGuid], + HttpContext.RequestAborted + )).FirstOrDefault(); - var articlesSection = await articlesSectionRepository.GetArticlesSection(articlesSectionItem.WebPageGuid, languageName, HttpContext.RequestAborted); if (articlesSection == null) { return View("~/Components/ViewComponents/Articles/Default.cshtml", ArticlesSectionViewModel.GetViewModel(null, Enumerable.Empty(), string.Empty)); } - var articlePages = await articlePageRepository.GetArticles(articlesSection.SystemFields.WebPageItemTreePath, - languageName, false, ARTICLES_PER_VIEW, HttpContext.RequestAborted); + IEnumerable articlePages = await GetArticlePages(articlesSection); var models = new List(); foreach (var article in articlePages) { - var model = await ArticleViewModel.GetViewModel(article, urlRetriever, languageName, HttpContext.RequestAborted); + var model = ArticleViewModel.GetViewModel(article); models.Add(model); } - var url = (await urlRetriever.Retrieve(articlesSection, languageName, HttpContext.RequestAborted)).RelativePath; - - var viewModel = ArticlesSectionViewModel.GetViewModel(articlesSection, models, url); + var viewModel = ArticlesSectionViewModel.GetViewModel(articlesSection, models, articlesSection.GetUrl().RelativePath); return View("~/Components/ViewComponents/Articles/Default.cshtml", viewModel); } + + private async Task> GetArticlePages(ArticlesSection articlesSection) + { + return await contentRetriever.RetrievePages( + new RetrievePagesParameters + { + LinkedItemsMaxLevel = 1, + PathMatch = PathMatch.Children(articlesSection.SystemFields.WebPageItemTreePath) + }, + query => query.TopN(ARTICLES_PER_VIEW), + new RetrievalCacheSettings($"TopN_{ARTICLES_PER_VIEW}"), + HttpContext.RequestAborted + ); + } } } diff --git a/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs index 21e673c..5064510 100644 --- a/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; -using System.Threading.Tasks; +using System.Linq; using System.Threading; +using System.Threading.Tasks; using CMS.Websites; using DancingGoat.Models; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; @@ -18,35 +19,33 @@ namespace DancingGoat.ViewComponents /// public class CafeCardSectionViewComponent : ViewComponent { - private readonly ContactsPageRepository contactsPageRepository; - private readonly IWebPageUrlRetriever webPageUrlRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; - - public CafeCardSectionViewComponent(IPreferredLanguageRetriever currentLanguageRetriever, ContactsPageRepository contactsPageRepository, IWebPageUrlRetriever webPageUrlRetriever) + public CafeCardSectionViewComponent(IContentRetriever contentRetriever) { - this.currentLanguageRetriever = currentLanguageRetriever; - this.contactsPageRepository = contactsPageRepository; - this.webPageUrlRetriever = webPageUrlRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync(IEnumerable cafes) { - string languageName = currentLanguageRetriever.Get(); - string contactsPagePath = await GetContactsPagePath(languageName, HttpContext.RequestAborted); + string contactsPagePath = await GetContactsPagePath(HttpContext.RequestAborted); var model = new CafeCardSectionViewModel(cafes, contactsPagePath); return View("~/Components/ViewComponents/CafeCardSection/Default.cshtml", model); } - private async Task GetContactsPagePath(string languageName, CancellationToken cancellationToken) + private async Task GetContactsPagePath(CancellationToken cancellationToken) { - const string CONTACTS_PAGE_TREE_PATH = "/Contacts"; - - var contactsPage = await contactsPageRepository.GetContactsPage(CONTACTS_PAGE_TREE_PATH, languageName, cancellationToken); - var url = await webPageUrlRetriever.Retrieve(contactsPage, cancellationToken); + var contactsPage = (await contentRetriever.RetrievePages( + RetrievePagesParameters.Default, + query => query.UrlPathColumns(), + new RetrievalCacheSettings("UrlPathColumns"), + cancellationToken + )).FirstOrDefault(); + + var url = contactsPage.GetUrl(); return url.RelativePath; } diff --git a/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs index 66c59e4..eb7efaf 100644 --- a/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs @@ -1,8 +1,9 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using DancingGoat.Models; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Microsoft.AspNetCore.Mvc; @@ -10,21 +11,21 @@ namespace DancingGoat.ViewComponents { public class CompanyAddressViewComponent : ViewComponent { - private readonly ContactRepository contactRepository; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; - public CompanyAddressViewComponent(ContactRepository contactRepository, IPreferredLanguageRetriever currentLanguageRetriever) + public CompanyAddressViewComponent(IContentRetriever contentRetriever) { - this.contactRepository = contactRepository; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync() { - var languageName = currentLanguageRetriever.Get(); - var contact = await contactRepository.GetContact(languageName, HttpContext.RequestAborted); + var contact = (await contentRetriever.RetrieveContent( + HttpContext.RequestAborted + )).FirstOrDefault(); + var model = ContactViewModel.GetViewModel(contact); return View("~/Components/ViewComponents/CompanyAddress/Default.cshtml", model); diff --git a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs index 0dbdf25..c8c679b 100644 --- a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs @@ -22,7 +22,7 @@ public async Task InvokeAsync() { var languageName = currentLanguageRetriever.Get(); - var navigationViewModels = await navigationService.GetNavigationItemViewModels(languageName, HttpContext.RequestAborted); + var navigationViewModels = await navigationService.GetSiteNavigationItemViewModels(languageName, HttpContext.RequestAborted); return View($"~/Components/ViewComponents/NavigationMenu/Default.cshtml", navigationViewModels); } diff --git a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs index 94f7c0b..2de22e5 100644 --- a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs +++ b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,76 +9,71 @@ using DancingGoat.Models; +using Kentico.Content.Web.Mvc; + namespace DancingGoat.ViewComponents { public class NavigationService { - private readonly NavigationItemRepository navigationItemRepository; + private readonly IContentRetriever contentRetriever; private readonly IWebPageUrlRetriever webPageUrlRetriever; - private readonly IProgressiveCache progressiveCache; private readonly IWebsiteChannelContext websiteChannelContext; - + private readonly IProgressiveCache progressiveCache; public NavigationService( - NavigationItemRepository navigationItemRepository, + IContentRetriever contentRetriever, IWebPageUrlRetriever webPageUrlRetriever, - IProgressiveCache progressiveCache, - IWebsiteChannelContext websiteChannelContext) + IWebsiteChannelContext websiteChannelContext, + IProgressiveCache progressiveCache) { - this.navigationItemRepository = navigationItemRepository; + this.contentRetriever = contentRetriever; this.webPageUrlRetriever = webPageUrlRetriever; - this.progressiveCache = progressiveCache; this.websiteChannelContext = websiteChannelContext; + this.progressiveCache = progressiveCache; } - public async Task> GetNavigationItemViewModels(string languageName, CancellationToken cancellationToken = default) + public async Task> GetSiteNavigationItemViewModels(string languageName, CancellationToken cancellationToken = default) { - var navigationItems = (await navigationItemRepository.GetNavigationItems(languageName, cancellationToken)) - .ToList(); - - var menuItemGuids = navigationItems - .Select(navigationItem => navigationItem.NavigationItemLink.First().WebPageGuid) - .ToList(); + return await GetNavigationItemViewModelsInternal(DancingGoatConstants.SITE_NAVIGATION_MENU_TREE_PATH, languageName, cancellationToken); + } - var navigationModels = await GetModelsCached(navigationItems, menuItemGuids, languageName, cancellationToken); - return navigationModels; + public async Task> GetStoreNavigationItemViewModels(string languageName, CancellationToken cancellationToken = default) + { + return await GetNavigationItemViewModelsInternal(DancingGoatConstants.STORE_NAVIGATION_MENU_TREE_PATH, languageName, cancellationToken); } - private async Task> GetModelsCached(List navigationItems, List menuItemGuids, string languageName, CancellationToken cancellationToken) + public async Task> GetNavigationItemViewModelsInternal(string treePath, string languageName, CancellationToken cancellationToken = default) { - var cacheSettings = new CacheSettings(5, websiteChannelContext.WebsiteChannelName, nameof(GetNavigationItemViewModels), languageName); - - return await progressiveCache.LoadAsync(async (settings, cancellationToken) => + using (var collector = new CacheDependencyCollector()) { - var urls = await webPageUrlRetriever.Retrieve(menuItemGuids, websiteChannelContext.WebsiteChannelName, languageName, cancellationToken: cancellationToken); - - var navigationModels = navigationItems - .Where(navigationItem => urls.ContainsKey(navigationItem.NavigationItemLink.First().WebPageGuid)) - .Select(navigationItem => - new NavigationItemViewModel( - navigationItem.NavigationItemName, - urls[navigationItem.NavigationItemLink.First().WebPageGuid].RelativePath - )); - - if (cacheSettings.Cached = navigationModels != null && navigationModels.Any()) + return await progressiveCache.LoadAsync(async (cacheSettings, cancellationToken) => { - var cacheKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var key in menuItemGuids) - { - cacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byguid", key.ToString() }, false)); - } - - cacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", websiteChannelContext.WebsiteChannelName, "childrenofpath", DancingGoatConstants.NAVIGATION_MENU_FOLDER_PATH })); - - cacheSettings.CacheDependency = CacheHelper.GetCacheDependency(cacheKeys); - } - - return navigationModels; - }, cacheSettings, cancellationToken); + var navigationItems = (await contentRetriever.RetrievePages( + new RetrievePagesParameters + { + PathMatch = PathMatch.Children(treePath, 1) + }, + query => query.OrderBy(nameof(IWebPageContentQueryDataContainer.WebPageItemOrder)), + RetrievalCacheSettings.CacheDisabled, + cancellationToken + )).ToList(); + + var urls = await webPageUrlRetriever + .Retrieve([.. navigationItems.SelectMany(x => x.NavigationItemLink.Select(y => y.WebPageGuid))], + websiteChannelContext.WebsiteChannelName, languageName, websiteChannelContext.IsPreview, cancellationToken); + + cacheSettings.CacheDependency = collector.GetCacheDependency(); + + return navigationItems.Select(x => new NavigationItemViewModel( + x.NavigationItemName, + urls[x.NavigationItemLink.First().WebPageGuid].RelativePath + )); + }, + new CacheSettings(10, $"NavigationItems_{treePath}"), cancellationToken); + } } } } diff --git a/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs index b480f59..53c4900 100644 --- a/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs @@ -3,7 +3,7 @@ using DancingGoat.Models; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Microsoft.AspNetCore.Mvc; @@ -11,21 +11,21 @@ namespace DancingGoat.ViewComponents { public class SocialLinksViewComponent : ViewComponent { - private readonly SocialLinkRepository socialLinkRepository; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; - public SocialLinksViewComponent(SocialLinkRepository socialLinkRepository, IPreferredLanguageRetriever currentLanguageRetriever) + + public SocialLinksViewComponent(IContentRetriever contentRetriever) { - this.socialLinkRepository = socialLinkRepository; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync() { - var languageName = currentLanguageRetriever.Get(); - - var socialLinks = await socialLinkRepository.GetSocialLinks(languageName, HttpContext.RequestAborted); + var socialLinks = await contentRetriever.RetrieveContent( + new RetrieveContentParameters { LinkedItemsMaxLevel = 1 }, + HttpContext.RequestAborted + ); return View("~/Components/ViewComponents/SocialLinks/Default.cshtml", socialLinks.Select(SocialLinkViewModel.GetViewModel)); } diff --git a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs index e008489..f4662d0 100644 --- a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs @@ -4,7 +4,7 @@ using DancingGoat.Models; using DancingGoat.Widgets; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; using Microsoft.AspNetCore.Mvc; @@ -25,26 +25,22 @@ public class CardWidgetViewComponent : ViewComponent public const string IDENTIFIER = "DancingGoat.LandingPage.CardWidget"; - private readonly ImageRepository imageRepository; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; /// /// Creates an instance of class. /// - /// Repository for images. - /// Retrieves preferred language name for the current request. Takes language fallback into account. - public CardWidgetViewComponent(ImageRepository imageRepository, IPreferredLanguageRetriever currentLanguageRetriever) + /// Content retriever. + public CardWidgetViewComponent(IContentRetriever contentRetriever) { - this.imageRepository = imageRepository; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync(CardWidgetProperties properties) { - var languageName = currentLanguageRetriever.Get(); - var image = await GetImage(properties, languageName); + var image = await GetImage(properties); return View("~/Components/Widgets/CardWidget/_CardWidget.cshtml", new CardWidgetViewModel { @@ -54,7 +50,7 @@ public async Task InvokeAsync(CardWidgetProperties prop } - private async Task GetImage(CardWidgetProperties properties, string languageName) + private async Task GetImage(CardWidgetProperties properties) { var image = properties.Image.FirstOrDefault(); @@ -63,7 +59,12 @@ private async Task GetImage(CardWidgetProperties properties, string langu return null; } - return await imageRepository.GetImage(image.Identifier, languageName); + var result = await contentRetriever.RetrieveContentByGuids( + [image.Identifier], + HttpContext.RequestAborted + ); + + return result.FirstOrDefault(); } } } diff --git a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs index 4e74f23..4f2344c 100644 --- a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs @@ -4,7 +4,7 @@ using DancingGoat.Models; using DancingGoat.Widgets; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; using Microsoft.AspNetCore.Mvc; @@ -24,26 +24,22 @@ public class HeroImageWidgetViewComponent : ViewComponent /// public const string IDENTIFIER = "DancingGoat.LandingPage.HeroImage"; - private readonly ImageRepository imageRepository; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; /// /// Creates an instance of class. /// - /// Repository for images. - /// Retrieves preferred language name for the current request. Takes language fallback into account. - public HeroImageWidgetViewComponent(ImageRepository imageRepository, IPreferredLanguageRetriever currentLanguageRetriever) + /// Content retriever. + public HeroImageWidgetViewComponent(IContentRetriever contentRetriever) { - this.imageRepository = imageRepository; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task InvokeAsync(HeroImageWidgetProperties properties) { - var languageName = currentLanguageRetriever.Get(); - var image = await GetImage(properties, languageName); + var image = await GetImage(properties); return View("~/Components/Widgets/HeroImageWidget/_HeroImageWidget.cshtml", new HeroImageWidgetViewModel { @@ -56,7 +52,7 @@ public async Task InvokeAsync(HeroImageWidgetProperties } - private async Task GetImage(HeroImageWidgetProperties properties, string languageName) + private async Task GetImage(HeroImageWidgetProperties properties) { var image = properties.Image.FirstOrDefault(); @@ -65,7 +61,12 @@ private async Task GetImage(HeroImageWidgetProperties properties, string return null; } - return await imageRepository.GetImage(image.Identifier, languageName); + var result = await contentRetriever.RetrieveContentByGuids( + [image.Identifier], + HttpContext.RequestAborted + ); + + return result.FirstOrDefault(); } } } diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs index 19936a5..94c0d6a 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs @@ -41,9 +41,9 @@ public static ProductCardViewModel GetViewModel(IProductFields product) return new ProductCardViewModel { - Heading = product.ProductFieldsName, - ImagePath = product.ProductFieldsImage.FirstOrDefault()?.ImageFile.Url, - Text = product.ProductFieldsShortDescription + Heading = product.ProductFieldName, + ImagePath = product.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, + Text = product.ProductFieldDescription }; } } diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs index 04a3e77..a48fd4b 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Threading; using System.Threading.Tasks; using CMS.ContentEngine; @@ -6,6 +7,7 @@ using DancingGoat.Models; using DancingGoat.Widgets; +using Kentico.Content.Web.Mvc; using Kentico.Content.Web.Mvc.Routing; using Kentico.PageBuilder.Web.Mvc; @@ -27,29 +29,41 @@ public class ProductCardWidgetViewComponent : ViewComponent public const string IDENTIFIER = "DancingGoat.LandingPage.ProductCardWidget"; - private readonly ProductRepository repository; + private readonly IContentRetriever contentRetriever; private readonly IPreferredLanguageRetriever currentLanguageRetriever; /// /// Creates an instance of class. /// - /// Repository for retrieving products. + /// Content retriever. /// Retrieves preferred language name for the current request. Takes language fallback into account. - public ProductCardWidgetViewComponent(ProductRepository repository, IPreferredLanguageRetriever currentLanguageRetriever) + public ProductCardWidgetViewComponent(IContentRetriever contentRetriever, IPreferredLanguageRetriever currentLanguageRetriever) { - this.repository = repository; + this.contentRetriever = contentRetriever; this.currentLanguageRetriever = currentLanguageRetriever; } - public async Task InvokeAsync(ProductCardProperties properties) + public async Task InvokeAsync(ProductCardProperties properties, CancellationToken cancellationToken) { var languageName = currentLanguageRetriever.Get(); var selectedProductGuids = properties.SelectedProducts.Select(i => i.Identifier).ToList(); - var products = (await repository.GetProducts(selectedProductGuids, languageName)) - .OrderBy(p => selectedProductGuids.IndexOf(((IContentItemFieldsSource)p).SystemFields.ContentItemGUID)); - var model = ProductCardListViewModel.GetViewModel(products); + + var products = await contentRetriever.RetrieveContentOfReusableSchemas( + [IProductFields.REUSABLE_FIELD_SCHEMA_NAME], + new RetrieveContentOfReusableSchemasParameters + { + LinkedItemsMaxLevel = 1, + WorkspaceNames = [DancingGoatConstants.COMMERCE_WORKSPACE_NAME] + }, + query => query.Where(where => where.WhereIn(nameof(IContentQueryDataContainer.ContentItemGUID), selectedProductGuids)), + new RetrievalCacheSettings($"WhereIn_{nameof(IContentQueryDataContainer.ContentItemGUID)}_{string.Join("_", selectedProductGuids)}"), + cancellationToken + ); + + var orderedProducts = products.OrderBy(p => selectedProductGuids.IndexOf(((IContentItemFieldsSource)p).SystemFields.ContentItemGUID)); + var model = ProductCardListViewModel.GetViewModel(orderedProducts); return View("~/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml", model); } diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml b/examples/DancingGoat/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml index b10377e..c42fd67 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml @@ -21,7 +21,9 @@ @if (!string.IsNullOrEmpty(product.Text)) { -

@product.Text

+
+ @Html.Raw(product.Text) +
} diff --git a/examples/DancingGoat/Controllers/AccountController.cs b/examples/DancingGoat/Controllers/AccountController.cs index f43a9c6..7c95ac2 100644 --- a/examples/DancingGoat/Controllers/AccountController.cs +++ b/examples/DancingGoat/Controllers/AccountController.cs @@ -1,16 +1,15 @@ using System; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using CMS.Core; -using CMS.DataEngine; using CMS.Websites; -using CMS.Websites.Routing; using DancingGoat.Models; -using Kentico.Content.Web.Mvc.Routing; +using Kentico.Content.Web.Mvc; using Kentico.Membership; using Microsoft.AspNetCore.Authorization; @@ -26,10 +25,7 @@ public class AccountController : Controller { private readonly IStringLocalizer localizer; private readonly IEventLogService eventLogService; - private readonly IInfoProvider websiteChannelProvider; - private readonly IWebPageUrlRetriever webPageUrlRetriever; - private readonly IWebsiteChannelContext websiteChannelContext; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; private readonly UserManager userManager; private readonly SignInManager signInManager; @@ -39,19 +35,13 @@ public AccountController( SignInManager signInManager, IStringLocalizer localizer, IEventLogService eventLogService, - IInfoProvider websiteChannelProvider, - IWebPageUrlRetriever webPageUrlRetriever, - IWebsiteChannelContext websiteChannelContext, - IPreferredLanguageRetriever preferredLanguageRetriever) + IContentRetriever contentRetriever) { this.userManager = userManager; this.signInManager = signInManager; this.localizer = localizer; this.eventLogService = eventLogService; - this.websiteChannelProvider = websiteChannelProvider; - this.webPageUrlRetriever = webPageUrlRetriever; - this.websiteChannelContext = websiteChannelContext; - this.currentLanguageRetriever = preferredLanguageRetriever; + this.contentRetriever = contentRetriever; } @@ -169,31 +159,16 @@ public async Task Register(RegisterViewModel model, CancellationTo } - private async Task GetHomeWebPageUrl(CancellationToken cancellationToken) + private async Task GetHomeWebPageUrl(CancellationToken cancellationToken = default) { - var websiteChannelId = websiteChannelContext.WebsiteChannelID; - var websiteChannel = await websiteChannelProvider.GetAsync(websiteChannelId, cancellationToken); - - if (websiteChannel == null) - { - return string.Empty; - } - - var homePageUrl = await webPageUrlRetriever.Retrieve( - websiteChannel.WebsiteChannelHomePage, - websiteChannelContext.WebsiteChannelName, - currentLanguageRetriever.Get(), - websiteChannelContext.IsPreview, + var homePage = (await contentRetriever.RetrievePages( + RetrievePagesParameters.Default, + query => query.UrlPathColumns(), + new RetrievalCacheSettings("UrlPathColumns"), cancellationToken - ); - - if (string.IsNullOrEmpty(homePageUrl?.RelativePath)) - { - return "/"; - } - - return homePageUrl.RelativePath; + )).FirstOrDefault(); + return homePage.GetUrl().RelativePath; } } } diff --git a/examples/DancingGoat/Controllers/DancingGoatArticleController.cs b/examples/DancingGoat/Controllers/DancingGoatArticleController.cs index 260fd39..3bbe39c 100644 --- a/examples/DancingGoat/Controllers/DancingGoatArticleController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatArticleController.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using CMS.DataEngine; using CMS.Websites; using DancingGoat; @@ -20,48 +22,26 @@ namespace DancingGoat.Controllers { public class DancingGoatArticleController : Controller { - private readonly ArticlePageRepository articlePageRepository; - private readonly ArticlesSectionRepository articlesSectionRepository; - private readonly IWebPageUrlRetriever urlRetriever; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; - - - public DancingGoatArticleController( - ArticlePageRepository articlePageRepository, - ArticlesSectionRepository articlesSectionRepository, - IWebPageUrlRetriever urlRetriever, - IWebPageDataContextRetriever webPageDataContextRetriever, - IPreferredLanguageRetriever currentLanguageRetriever) + private readonly IContentRetriever contentRetriever; + + + public DancingGoatArticleController(IContentRetriever contentRetriever) { - this.articlePageRepository = articlePageRepository; - this.articlesSectionRepository = articlesSectionRepository; - this.urlRetriever = urlRetriever; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } public async Task Index() { - var languageName = currentLanguageRetriever.Get(); - - var webPage = webPageDataContextRetriever.Retrieve().WebPage; + var articlesSection = await contentRetriever.RetrieveCurrentPage( + HttpContext.RequestAborted + ); - var articlesSection = await articlesSectionRepository.GetArticlesSection(webPage.WebPageItemID, languageName, HttpContext.RequestAborted); - - var articles = await articlePageRepository.GetArticles(articlesSection.SystemFields.WebPageItemTreePath, languageName, true, cancellationToken: HttpContext.RequestAborted); - - var models = new List(); - foreach (var article in articles) - { - var articleModel = await ArticleViewModel.GetViewModel(article, urlRetriever, languageName); - models.Add(articleModel); - } + var articles = await GetArticles(articlesSection); - var url = (await urlRetriever.Retrieve(articlesSection, languageName)).RelativePath; + var models = articles.Select(ArticleViewModel.GetViewModel); - var model = ArticlesSectionViewModel.GetViewModel(articlesSection, models, url); + var model = ArticlesSectionViewModel.GetViewModel(articlesSection, models, articlesSection.GetUrl().RelativePath); return View(model); } @@ -69,19 +49,35 @@ public async Task Index() public async Task Article() { - var languageName = currentLanguageRetriever.Get(); - var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - - var article = await articlePageRepository.GetArticle(webPageItemId, languageName, HttpContext.RequestAborted); + var article = await contentRetriever.RetrieveCurrentPage( + new RetrieveCurrentPageParameters { IncludeSecuredItems = true, LinkedItemsMaxLevel = 3 }, + HttpContext.RequestAborted + ); if (article is null) { return NotFound(); } - var model = await ArticleDetailViewModel.GetViewModel(article, languageName, articlePageRepository, urlRetriever); + var model = ArticleDetailViewModel.GetViewModel(article); return new TemplateResult(model); } + + + private async Task> GetArticles(ArticlesSection articlesSection) + { + return await contentRetriever.RetrievePages( + new RetrievePagesParameters + { + PathMatch = PathMatch.Children(articlesSection.SystemFields.WebPageItemTreePath), + IncludeSecuredItems = true, + LinkedItemsMaxLevel = 1 + }, + query => query.OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate))), + new RetrievalCacheSettings($"OrderBy_{nameof(ArticlePage.ArticlePagePublishDate)}_{nameof(OrderByColumn.Desc)}"), + HttpContext.RequestAborted + ); + } } } diff --git a/examples/DancingGoat/Controllers/DancingGoatCoffeeController.cs b/examples/DancingGoat/Controllers/DancingGoatCoffeeController.cs deleted file mode 100644 index 75421af..0000000 --- a/examples/DancingGoat/Controllers/DancingGoatCoffeeController.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; - -using Microsoft.AspNetCore.Mvc; - -using CMS.ContentEngine; - -using Kentico.Content.Web.Mvc; -using Kentico.Content.Web.Mvc.Routing; - -using DancingGoat; -using DancingGoat.Controllers; -using DancingGoat.Models; - -[assembly: RegisterWebPageRoute(CoffeePage.CONTENT_TYPE_NAME, typeof(DancingGoatCoffeeController), WebsiteChannelNames = new[] { DancingGoatConstants.WEBSITE_CHANNEL_NAME }, ActionName = nameof(DancingGoatCoffeeController.Detail))] - -namespace DancingGoat.Controllers -{ - public class DancingGoatCoffeeController : Controller - { - private readonly ProductPageRepository productPageRepository; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; - private readonly ITaxonomyRetriever taxonomyRetriever; - - - public DancingGoatCoffeeController(ProductPageRepository productPageRepository, - IWebPageDataContextRetriever webPageDataContextRetriever, - IPreferredLanguageRetriever currentLanguageRetriever, - ITaxonomyRetriever taxonomyRetriever) - { - this.productPageRepository = productPageRepository; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; - this.taxonomyRetriever = taxonomyRetriever; - } - - - public async Task Detail() - { - var languageName = currentLanguageRetriever.Get(); - var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - - var coffee = await productPageRepository.GetProduct(CoffeePage.CONTENT_TYPE_NAME, webPageItemId, languageName, cancellationToken: HttpContext.RequestAborted); - - return View(await CoffeeDetailViewModel.GetViewModel(coffee, languageName, taxonomyRetriever)); - } - } -} diff --git a/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs b/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs index 2a4d559..b37e474 100644 --- a/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs @@ -15,24 +15,19 @@ namespace DancingGoat.Controllers { public class DancingGoatConfirmationController : Controller { - private readonly ConfirmationPageRepository confirmationPageRepository; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; - public DancingGoatConfirmationController(ConfirmationPageRepository confirmationPageRepository, IWebPageDataContextRetriever webPageDataContextRetriever, IPreferredLanguageRetriever currentLanguageRetriever) + public DancingGoatConfirmationController(IContentRetriever contentRetriever) { - this.confirmationPageRepository = confirmationPageRepository; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } - public async Task Index() { - var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - var languageName = currentLanguageRetriever.Get(); - - var confirmationPage = await confirmationPageRepository.GetConfirmationPage(webPageItemId, languageName, cancellationToken: HttpContext.RequestAborted); + var confirmationPage = await contentRetriever.RetrieveCurrentPage( + new RetrieveCurrentPageParameters { IncludeSecuredItems = true }, + HttpContext.RequestAborted + ); return View(ConfirmationPageViewModel.GetViewModel(confirmationPage)); } diff --git a/examples/DancingGoat/Controllers/DancingGoatContactsController.cs b/examples/DancingGoat/Controllers/DancingGoatContactsController.cs index ceadd78..f680721 100644 --- a/examples/DancingGoat/Controllers/DancingGoatContactsController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatContactsController.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Linq; using System.Threading.Tasks; using DancingGoat; @@ -18,57 +16,35 @@ namespace DancingGoat.Controllers { public class DancingGoatContactsController : Controller { - private readonly ContactsPageRepository contactsPageRepository; - private readonly ContactRepository contactRepository; - private readonly CafeRepository cafeRepository; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; + private readonly IContentRetriever contentRetriever; - - public DancingGoatContactsController(ContactsPageRepository contactsPageRepository, ContactRepository contactRepository, - CafeRepository cafeRepository, IPreferredLanguageRetriever currentLanguageRetriever, IWebPageDataContextRetriever webPageDataContextRetriever) + public DancingGoatContactsController(IContentRetriever contentRetriever) { - this.contactsPageRepository = contactsPageRepository; - this.contactRepository = contactRepository; - this.cafeRepository = cafeRepository; - this.currentLanguageRetriever = currentLanguageRetriever; - this.webPageDataContextRetriever = webPageDataContextRetriever; + this.contentRetriever = contentRetriever; } - - public async Task Index(CancellationToken cancellationToken) + public async Task Index() { - var webPage = webPageDataContextRetriever.Retrieve().WebPage; - - var contactsPage = await contactsPageRepository.GetContactsPage(webPage.WebPageItemID, webPage.LanguageName, HttpContext.RequestAborted); + var contactsPage = await contentRetriever.RetrieveCurrentPage(); - var model = await GetIndexViewModel(contactsPage, cancellationToken); + var cafes = await contentRetriever.RetrieveContent(); - return View(model); - } + var contact = (await contentRetriever.RetrieveContent( + HttpContext.RequestAborted + )).FirstOrDefault(); + var companyCafes = cafes.Where(c => c.CafeIsCompanyCafe).OrderBy(c => c.CafeName).Select(CafeViewModel.GetViewModel).ToList(); + var partnerCafes = cafes.Where(c => !c.CafeIsCompanyCafe).OrderBy(c => c.CafeCity).Select(CafeViewModel.GetViewModel).ToList(); - private async Task GetIndexViewModel(ContactsPage contactsPage, CancellationToken cancellationToken) - { - var languageName = currentLanguageRetriever.Get(); - var cafes = (await cafeRepository.GetCafes(0, languageName, cancellationToken)).ToList(); - var companyCafes = cafes.Where(c => c.CafeIsCompanyCafe).OrderBy(c => c.CafeName); - var partnerCafes = cafes.Where(c => !c.CafeIsCompanyCafe).OrderBy(c => c.CafeCity); - var contact = await contactRepository.GetContact(languageName, HttpContext.RequestAborted); - - return new ContactsIndexViewModel + var model = new ContactsIndexViewModel { + WebPage = contactsPage, CompanyContact = ContactViewModel.GetViewModel(contact), - CompanyCafes = GetCafesModel(companyCafes), - PartnerCafes = GetCafesModel(partnerCafes), - WebPage = contactsPage + CompanyCafes = companyCafes, + PartnerCafes = partnerCafes }; - } - - private List GetCafesModel(IEnumerable cafes) - { - return cafes.Select(cafe => CafeViewModel.GetViewModel(cafe)).ToList(); + return View(model); } } } diff --git a/examples/DancingGoat/Controllers/DancingGoatGrinderController.cs b/examples/DancingGoat/Controllers/DancingGoatGrinderController.cs deleted file mode 100644 index be3f1a4..0000000 --- a/examples/DancingGoat/Controllers/DancingGoatGrinderController.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; - -using Microsoft.AspNetCore.Mvc; - -using CMS.ContentEngine; - -using Kentico.Content.Web.Mvc; -using Kentico.Content.Web.Mvc.Routing; - -using DancingGoat; -using DancingGoat.Controllers; -using DancingGoat.Models; - -[assembly: RegisterWebPageRoute(GrinderPage.CONTENT_TYPE_NAME, typeof(DancingGoatGrinderController), WebsiteChannelNames = new[] { DancingGoatConstants.WEBSITE_CHANNEL_NAME }, ActionName = nameof(DancingGoatGrinderController.Detail))] - -namespace DancingGoat.Controllers -{ - public class DancingGoatGrinderController : Controller - { - private readonly ProductPageRepository productPageRepository; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; - private readonly ITaxonomyRetriever taxonomyRetriever; - - - public DancingGoatGrinderController(ProductPageRepository productPageRepository, - IWebPageDataContextRetriever webPageDataContextRetriever, - IPreferredLanguageRetriever currentLanguageRetriever, - ITaxonomyRetriever taxonomyRetriever) - { - this.productPageRepository = productPageRepository; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; - this.taxonomyRetriever = taxonomyRetriever; - } - - - public async Task Detail() - { - var languageName = currentLanguageRetriever.Get(); - var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - - var grinder = await productPageRepository.GetProduct(GrinderPage.CONTENT_TYPE_NAME, webPageItemId, languageName, cancellationToken: HttpContext.RequestAborted); - - return View(await GrinderDetailViewModel.GetViewModel(grinder, languageName, taxonomyRetriever)); - } - } -} diff --git a/examples/DancingGoat/Controllers/DancingGoatHomeController.cs b/examples/DancingGoat/Controllers/DancingGoatHomeController.cs index 68ee611..b24e7e5 100644 --- a/examples/DancingGoat/Controllers/DancingGoatHomeController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatHomeController.cs @@ -1,4 +1,11 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using CMS.ContentEngine; +using CMS.DataEngine; +using CMS.Helpers; +using CMS.Websites; using DancingGoat; using DancingGoat.Controllers; @@ -15,23 +22,46 @@ namespace DancingGoat.Controllers { public class DancingGoatHomeController : Controller { - private readonly HomePageRepository homePageRepository; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; + private readonly IContentRetriever contentRetriever; + private readonly ICacheDependencyBuilderFactory cacheDependencyBuilderFactory; - public DancingGoatHomeController(HomePageRepository homePageRepository, IWebPageDataContextRetriever webPageDataContextRetriever) + public DancingGoatHomeController(IContentRetriever contentRetriever, ICacheDependencyBuilderFactory cacheDependencyBuilderFactory) { - this.homePageRepository = homePageRepository; - this.webPageDataContextRetriever = webPageDataContextRetriever; + this.contentRetriever = contentRetriever; + this.cacheDependencyBuilderFactory = cacheDependencyBuilderFactory; } - public async Task Index() { - var webPage = webPageDataContextRetriever.Retrieve().WebPage; + var homePage = await contentRetriever.RetrieveCurrentPage( + new RetrieveCurrentPageParameters { LinkedItemsMaxLevel = 4 }, + HttpContext.RequestAborted + ); + + var cafes = await GetCafes(homePage); - var homePage = await homePageRepository.GetHomePage(webPage.WebPageItemID, webPage.LanguageName, HttpContext.RequestAborted); + return View(HomePageViewModel.GetViewModel(homePage, cafes)); + } + + private async Task> GetCafes(HomePage homePage) + { + var cafeAdditionalDependencies = cacheDependencyBuilderFactory.Create() + .ForWebPageItems() + .ByIdWithLanguageContext(homePage.SystemFields.WebPageItemID) + .Builder() + .ForInfoObjects() + .ByGuid(homePage.HomePageCafesFolder.Identifier) + .Builder() + .Build(); - return View(HomePageViewModel.GetViewModel(homePage)); + return await contentRetriever.RetrieveContent( + new RetrieveContentParameters { LinkedItemsMaxLevel = 1 }, + query => query + .InSmartFolder(homePage.HomePageCafesFolder.Identifier) + .TopN(3), + new RetrievalCacheSettings($"InSmartFolder_{homePage.HomePageCafesFolder.Identifier}_TopN_3", TimeSpan.FromMinutes(5), additionalCacheDependencies: cafeAdditionalDependencies), + HttpContext.RequestAborted + ); } } } diff --git a/examples/DancingGoat/Controllers/DancingGoatLandingPageController.cs b/examples/DancingGoat/Controllers/DancingGoatLandingPageController.cs index 5dd7951..0ec4e15 100644 --- a/examples/DancingGoat/Controllers/DancingGoatLandingPageController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatLandingPageController.cs @@ -16,26 +16,16 @@ namespace DancingGoat.Controllers { public class DancingGoatLandingPageController : Controller { - private readonly LandingPageRepository landingPageRepository; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly IContentRetriever contentRetriever; - - public DancingGoatLandingPageController(LandingPageRepository landingPageRepository, IWebPageDataContextRetriever webPageDataContextRetriever, IPreferredLanguageRetriever currentLanguageRetriever) + public DancingGoatLandingPageController(IContentRetriever contentRetriever) { - this.landingPageRepository = landingPageRepository; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; + this.contentRetriever = contentRetriever; } - public async Task Index() { - var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - var languageName = currentLanguageRetriever.Get(); - - var landingPage = await landingPageRepository.GetLandingPage(webPageItemId, languageName, cancellationToken: HttpContext.RequestAborted); - + var landingPage = await contentRetriever.RetrieveCurrentPage(HttpContext.RequestAborted); return new TemplateResult(landingPage); } } diff --git a/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs b/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs index 992fd9c..c5a33f5 100644 --- a/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs @@ -27,9 +27,8 @@ public class DancingGoatPrivacyController : Controller private readonly IConsentAgreementService consentAgreementService; private readonly IInfoProvider consentInfoProvider; + private readonly IContentRetriever contentRetriever; private readonly IPreferredLanguageRetriever currentLanguageRetriever; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly PrivacyPageRepository privacyPageRepository; private ContactInfo currentContact; @@ -47,21 +46,18 @@ private ContactInfo CurrentContact } - public DancingGoatPrivacyController(PrivacyPageRepository privacyPageRepository, IConsentAgreementService consentAgreementService, IInfoProvider consentInfoProvider, IPreferredLanguageRetriever currentLanguageRetriever, IWebPageDataContextRetriever webPageDataContextRetriever) + public DancingGoatPrivacyController(IContentRetriever contentRetriever, IConsentAgreementService consentAgreementService, IInfoProvider consentInfoProvider, IPreferredLanguageRetriever currentLanguageRetriever) { - this.privacyPageRepository = privacyPageRepository; + this.contentRetriever = contentRetriever; this.consentAgreementService = consentAgreementService; this.consentInfoProvider = consentInfoProvider; this.currentLanguageRetriever = currentLanguageRetriever; - this.webPageDataContextRetriever = webPageDataContextRetriever; } public async Task Index() { - var webPage = webPageDataContextRetriever.Retrieve().WebPage; - - var privacyPage = await privacyPageRepository.GetPrivacyPage(webPage.WebPageItemID, webPage.LanguageName, HttpContext.RequestAborted); + var privacyPage = await contentRetriever.RetrieveCurrentPage(HttpContext.RequestAborted); var model = new PrivacyViewModel { WebPage = privacyPage }; diff --git a/examples/DancingGoat/Controllers/DancingGoatProductCategoryController.cs b/examples/DancingGoat/Controllers/DancingGoatProductCategoryController.cs new file mode 100644 index 0000000..24430fd --- /dev/null +++ b/examples/DancingGoat/Controllers/DancingGoatProductCategoryController.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat; +using DancingGoat.Controllers; +using DancingGoat.Models; +using DancingGoat.ViewComponents; + +using Kentico.Content.Web.Mvc; +using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Mvc; + +[assembly: RegisterWebPageRoute(ProductCategory.CONTENT_TYPE_NAME, typeof(DancingGoatProductCategoryController), WebsiteChannelNames = [DancingGoatConstants.WEBSITE_CHANNEL_NAME])] + +namespace DancingGoat.Controllers +{ + public class DancingGoatProductCategoryController : Controller + { + private readonly IContentRetriever contentRetriever; + private readonly NavigationService navigationService; + private readonly ITaxonomyRetriever taxonomyRetriever; + private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly ProductRepository productRepository; + + private const string PRODUCT_CATEGORY_FIELD_NAME = "ProductFieldCategory"; + private const string PRODUCT_TYPE_SELECTION_OTHER_VALUE = "Other"; + + + public DancingGoatProductCategoryController( + IContentRetriever contentRetriever, + NavigationService navigationService, + ITaxonomyRetriever taxonomyRetriever, + IPreferredLanguageRetriever currentLanguageRetriever, + ProductRepository productRepository) + { + this.contentRetriever = contentRetriever; + this.navigationService = navigationService; + this.taxonomyRetriever = taxonomyRetriever; + this.currentLanguageRetriever = currentLanguageRetriever; + this.productRepository = productRepository; + } + + + public async Task Index(CancellationToken cancellationToken) + { + var languageName = currentLanguageRetriever.Get(); + + var productCategoryPage = await contentRetriever.RetrieveCurrentPage( + new RetrieveCurrentPageParameters { LinkedItemsMaxLevel = 1 }, + cancellationToken + ); + + var tagCollection = await TagCollection.Create(productCategoryPage.ProductCategoryTag.Select(t => t.Identifier)); + + var products = await GetProductsByTags(productCategoryPage, tagCollection, cancellationToken); + + var productPageUrls = await productRepository.GetProductPageUrls(products.Cast().Select(p => p.SystemFields.ContentItemID), cancellationToken); + + var productTagsTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(DancingGoatTaxonomyConstants.PRODUCT_TAGS_TAXONOMY_NAME, languageName, cancellationToken); + + var categoryMenu = await navigationService.GetStoreNavigationItemViewModels(languageName, cancellationToken); + + return View(ProductListingViewModel.GetViewModel(productCategoryPage, products, productPageUrls, productTagsTaxonomy, categoryMenu, languageName)); + } + + + public async Task> GetProductsByTags(ProductCategory productCategoryPage, + TagCollection tagCollection, CancellationToken cancellationToken = default) + { + var products = productCategoryPage.ProductType.Equals(PRODUCT_TYPE_SELECTION_OTHER_VALUE, StringComparison.InvariantCultureIgnoreCase) + ? await contentRetriever.RetrieveContentOfReusableSchemas( + [IProductFields.REUSABLE_FIELD_SCHEMA_NAME], + new RetrieveContentOfReusableSchemasParameters + { + LinkedItemsMaxLevel = 1, + WorkspaceNames = [DancingGoatConstants.COMMERCE_WORKSPACE_NAME] + }, + query => query.Where(where => where.WhereContainsTags(PRODUCT_CATEGORY_FIELD_NAME, tagCollection)), + new RetrievalCacheSettings($"WhereContainsTags_{PRODUCT_CATEGORY_FIELD_NAME}_{string.Join("_", tagCollection.TagIdentifiers)}"), + cancellationToken + ) + : await contentRetriever.RetrieveContentOfContentTypes( + [productCategoryPage.ProductType], + new RetrieveContentOfContentTypesParameters + { + LinkedItemsMaxLevel = 1, + WorkspaceNames = [DancingGoatConstants.COMMERCE_WORKSPACE_NAME] + }, + query => query.Where(where => where.WhereContainsTags(PRODUCT_CATEGORY_FIELD_NAME, tagCollection)), + new RetrievalCacheSettings($"WhereContainsTags_{PRODUCT_CATEGORY_FIELD_NAME}_{string.Join("_", tagCollection.TagIdentifiers)}"), + cancellationToken + ); + + return products; + } + } +} diff --git a/examples/DancingGoat/Controllers/DancingGoatProductController.cs b/examples/DancingGoat/Controllers/DancingGoatProductController.cs deleted file mode 100644 index 511c03d..0000000 --- a/examples/DancingGoat/Controllers/DancingGoatProductController.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Websites; - -using DancingGoat; -using DancingGoat.Controllers; -using DancingGoat.Models; - -using Kentico.Content.Web.Mvc; -using Kentico.Content.Web.Mvc.Routing; - -using Microsoft.AspNetCore.Mvc; - -[assembly: RegisterWebPageRoute(ProductsSection.CONTENT_TYPE_NAME, typeof(DancingGoatProductController), WebsiteChannelNames = new[] { DancingGoatConstants.WEBSITE_CHANNEL_NAME })] - -namespace DancingGoat.Controllers -{ - public class DancingGoatProductController : Controller - { - private readonly ProductSectionRepository productSectionRepository; - private readonly ProductPageRepository productPageRepository; - private readonly ProductRepository productRepository; - private readonly ITaxonomyRetriever taxonomyRetriever; - private readonly IWebPageUrlRetriever urlRetriever; - private readonly IWebPageDataContextRetriever webPageDataContextRetriever; - private readonly IPreferredLanguageRetriever currentLanguageRetriever; - - - public DancingGoatProductController( - ProductSectionRepository productSectionRepository, - ProductPageRepository productPageRepository, - ProductRepository productRepository, - IWebPageUrlRetriever urlRetriever, - IPreferredLanguageRetriever currentLanguageRetriever, - IWebPageDataContextRetriever webPageDataContextRetriever, - ITaxonomyRetriever taxonomyRetriever) - { - this.productSectionRepository = productSectionRepository; - this.productPageRepository = productPageRepository; - this.productRepository = productRepository; - this.urlRetriever = urlRetriever; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; - this.taxonomyRetriever = taxonomyRetriever; - } - - - public async Task Index() - { - var languageName = currentLanguageRetriever.Get(); - var webPage = webPageDataContextRetriever.Retrieve().WebPage; - var productsSection = await productSectionRepository.GetProductsSection(webPage.WebPageItemID, languageName, HttpContext.RequestAborted); - - var products = await GetProducts(languageName, productsSection); - - var taxonomies = new Dictionary(); - var taxonomyNames = new List { "CoffeeProcessing", "CoffeeTastes", "GrinderManufacturer", "GrinderType" }; - foreach (var taxonomyName in taxonomyNames) - { - var taxonomy = await taxonomyRetriever.RetrieveTaxonomy(taxonomyName, languageName); - if (taxonomy.Tags.Any()) - { - taxonomies.Add(taxonomyName, TaxonomyViewModel.GetViewModel(taxonomy)); - } - } - - var listModel = new ProductListViewModel(products, taxonomies); - - return View(listModel); - } - - - [HttpPost($"{{{WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY}}}/{{controller}}/{{action}}")] - [ValidateAntiForgeryToken] - public async Task Filter(IDictionary filter) - { - var languageName = currentLanguageRetriever.Get(); - var webPage = webPageDataContextRetriever.Retrieve().WebPage; - var productsSection = await productSectionRepository.GetProductsSection(webPage.WebPageItemID, languageName, HttpContext.RequestAborted); - - var products = await GetProducts(languageName, productsSection, filter); - return PartialView("ProductsList", products); - } - - - private async Task> GetProducts(string languageName, ProductsSection productsSection, IDictionary filter = null) - { - var products = await productRepository.GetProducts(languageName, filter ?? new Dictionary(), cancellationToken: HttpContext.RequestAborted); - var productPages = await productPageRepository.GetProducts(productsSection.SystemFields.WebPageItemTreePath, languageName, products, cancellationToken: HttpContext.RequestAborted); - - return productPages.Select(productPage => ProductListItemViewModel.GetViewModel(productPage, urlRetriever, languageName).Result); - } - } -} diff --git a/examples/DancingGoat/Controllers/DancingGoatProductDetailController.cs b/examples/DancingGoat/Controllers/DancingGoatProductDetailController.cs new file mode 100644 index 0000000..72ed3f0 --- /dev/null +++ b/examples/DancingGoat/Controllers/DancingGoatProductDetailController.cs @@ -0,0 +1,72 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat; +using DancingGoat.Commerce; +using DancingGoat.Controllers; +using DancingGoat.Models; +using DancingGoat.Services; + +using Kentico.Content.Web.Mvc; +using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Mvc; + +[assembly: RegisterWebPageRoute(ProductPage.CONTENT_TYPE_NAME, typeof(DancingGoatProductDetailController), WebsiteChannelNames = [DancingGoatConstants.WEBSITE_CHANNEL_NAME])] + +namespace DancingGoat.Controllers +{ + public class DancingGoatProductDetailController : Controller + { + private readonly IContentRetriever contentRetriever; + private readonly ProductParametersExtractor productParametersExtractor; + private readonly ProductVariantsExtractor productVariantsExtractor; + private readonly TagTitleRetriever tagTitleRetriever; + private readonly IPreferredLanguageRetriever currentLanguageRetriever; + + + public DancingGoatProductDetailController( + IContentRetriever contentRetriever, + ProductParametersExtractor productParametersExtractor, + ProductVariantsExtractor productVariantsExtractor, + TagTitleRetriever tagTitleRetriever, + IPreferredLanguageRetriever currentLanguageRetriever) + { + this.contentRetriever = contentRetriever; + this.productParametersExtractor = productParametersExtractor; + this.productVariantsExtractor = productVariantsExtractor; + this.tagTitleRetriever = tagTitleRetriever; + this.currentLanguageRetriever = currentLanguageRetriever; + } + + + public async Task Index(CancellationToken cancellationToken) + { + var languageName = currentLanguageRetriever.Get(); + var productPage = await contentRetriever.RetrieveCurrentPage( + new RetrieveCurrentPageParameters { LinkedItemsMaxLevel = 2 }, + cancellationToken + ); + + if (productPage == null || !productPage.ProductPageProduct.Any()) + { + return NotFound(); + } + + var productItem = productPage.ProductPageProduct.FirstOrDefault() as IProductFields; + + var tag = productItem.ProductFieldTags.Any() ? await tagTitleRetriever.GetTagTitle(productItem.ProductFieldTags.First().Identifier, languageName, cancellationToken) : null; + + var parameters = await productParametersExtractor.ExtractParameters(productItem, languageName, cancellationToken); + + var variantValues = productVariantsExtractor.ExtractVariantsValue(productItem); + + int contentItemId = (productItem as IContentItemFieldsSource).SystemFields.ContentItemID; + + return View(new ProductViewModel(productItem.ProductFieldName, productItem.ProductFieldDescription, productItem.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, productItem.ProductFieldPrice, tag, contentItemId, parameters, variantValues)); + } + } +} diff --git a/examples/DancingGoat/Controllers/DancingGoatProductSectionController.cs b/examples/DancingGoat/Controllers/DancingGoatProductSectionController.cs new file mode 100644 index 0000000..df792c6 --- /dev/null +++ b/examples/DancingGoat/Controllers/DancingGoatProductSectionController.cs @@ -0,0 +1,51 @@ +using System.Threading; +using System.Threading.Tasks; + +using CMS.Websites.Routing; + +using DancingGoat; +using DancingGoat.Controllers; +using DancingGoat.Models; +using DancingGoat.Services; + +using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; + +[assembly: RegisterWebPageRoute(ProductsSection.CONTENT_TYPE_NAME, typeof(DancingGoatProductSectionController), WebsiteChannelNames = [DancingGoatConstants.WEBSITE_CHANNEL_NAME])] + +namespace DancingGoat.Controllers +{ + public class DancingGoatProductSectionController : Controller + { + private readonly IWebsiteChannelContext websiteChannelContext; + private readonly WebPageUrlProvider webPageUrlProvider; + private readonly IStringLocalizer localizer; + + + public DancingGoatProductSectionController( + WebPageUrlProvider webPageUrlProvider, + IWebsiteChannelContext websiteChannelContext, + IStringLocalizer localizer) + { + this.webPageUrlProvider = webPageUrlProvider; + this.websiteChannelContext = websiteChannelContext; + this.localizer = localizer; + } + + + public async Task Index(CancellationToken cancellationToken) + { + if (websiteChannelContext.IsPreview) + { + return Content(localizer["Redirection to the Store page when on the live site."]); + } + + var storePageUrl = await webPageUrlProvider.StorePageUrl(cancellationToken: cancellationToken); + + // Redirect to the store page + return Redirect(storePageUrl); + } + } +} diff --git a/examples/DancingGoat/Controllers/DancingGoatStoreController.cs b/examples/DancingGoat/Controllers/DancingGoatStoreController.cs new file mode 100644 index 0000000..cd25878 --- /dev/null +++ b/examples/DancingGoat/Controllers/DancingGoatStoreController.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +using DancingGoat; +using DancingGoat.Controllers; +using DancingGoat.Models; +using DancingGoat.ViewComponents; + +using Kentico.Content.Web.Mvc; +using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Mvc; + +using static DancingGoat.ProductTagTaxonomyConstants; + +[assembly: RegisterWebPageRoute(Store.CONTENT_TYPE_NAME, typeof(DancingGoatStoreController), WebsiteChannelNames = [DancingGoatConstants.WEBSITE_CHANNEL_NAME])] + +namespace DancingGoat.Controllers +{ + public class DancingGoatStoreController : Controller + { + private readonly IContentRetriever contentRetriever; + private readonly NavigationService navigationService; + private readonly ITaxonomyRetriever taxonomyRetriever; + private readonly IPreferredLanguageRetriever currentLanguageRetriever; + private readonly ProductRepository productRepository; + + + private const string PRODUCT_TAGS_FIELD_NAME = "ProductFieldTags"; + private readonly string[] PRODUCT_TAGS_TO_DISPLAY = [TAG_NAME_BESTSELLER, TAG_NAME_HOT_TIPS]; + + + public DancingGoatStoreController(IContentRetriever contentRetriever, NavigationService navigationService, + ITaxonomyRetriever taxonomyRetriever, IPreferredLanguageRetriever currentLanguageRetriever, + ProductRepository productRepository) + { + this.contentRetriever = contentRetriever; + this.navigationService = navigationService; + this.taxonomyRetriever = taxonomyRetriever; + this.currentLanguageRetriever = currentLanguageRetriever; + this.productRepository = productRepository; + } + + + public async Task Index(CancellationToken cancellationToken) + { + var storePage = await contentRetriever.RetrieveCurrentPage(cancellationToken); + var languageName = currentLanguageRetriever.Get(); + + var tagCollection = await TagCollection.Create(PRODUCT_TAGS_TO_DISPLAY); + var products = await GetProductsByTags(tagCollection, cancellationToken); + + var productPageUrls = await productRepository.GetProductPageUrls(products.Cast().Select(p => p.SystemFields.ContentItemID), cancellationToken); + + var categoryMenu = await navigationService.GetStoreNavigationItemViewModels(languageName, cancellationToken); + + var productTagsTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(DancingGoatTaxonomyConstants.PRODUCT_TAGS_TAXONOMY_NAME, languageName, cancellationToken); + + return View(StoreViewModel.GetViewModel(storePage, products, productPageUrls, PRODUCT_TAGS_TO_DISPLAY, productTagsTaxonomy, languageName, categoryMenu)); + } + + + private async Task> GetProductsByTags(TagCollection tagCollection, CancellationToken cancellationToken = default) + { + var products = await contentRetriever.RetrieveContentOfReusableSchemas( + [IProductFields.REUSABLE_FIELD_SCHEMA_NAME], + new RetrieveContentOfReusableSchemasParameters + { + LinkedItemsMaxLevel = 1, + WorkspaceNames = [DancingGoatConstants.COMMERCE_WORKSPACE_NAME] + }, + query => query.Where(where => where.WhereContainsTags(PRODUCT_TAGS_FIELD_NAME, tagCollection)), + new RetrievalCacheSettings($"WhereContainsTags_{PRODUCT_TAGS_FIELD_NAME}_{string.Join("_", PRODUCT_TAGS_TO_DISPLAY)}"), + cancellationToken + ); + + return products; + } + } +} diff --git a/examples/DancingGoat/Controllers/SiteMapController.cs b/examples/DancingGoat/Controllers/SiteMapController.cs index 28d5fdc..33fa5e6 100644 --- a/examples/DancingGoat/Controllers/SiteMapController.cs +++ b/examples/DancingGoat/Controllers/SiteMapController.cs @@ -1,16 +1,18 @@ -using System.Collections.Generic; -using System.Linq; +using System; +using System.Collections.Generic; +using System.Net.Mime; +using System.Text; using System.Threading.Tasks; using System.Xml; using CMS.ContentEngine; -using CMS.DataEngine; +using CMS.Helpers; using CMS.Websites; +using CMS.Websites.Routing; using DancingGoat.Models; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; namespace DancingGoat.Controllers @@ -20,21 +22,22 @@ namespace DancingGoat.Controllers /// public class SiteMapController : Controller { - private const string XML_TYPE = "application/xml"; - private readonly IContentQueryExecutor contentQueryExecutor; - private readonly IWebPageUrlRetriever urlRetriever; - private readonly IInfoProvider contentLanguageProvider; - + private readonly IProgressiveCache progressiveCache; + private readonly IWebsiteChannelContext websiteChannelContext; + private static readonly double cacheMinutes = TimeSpan.FromDays(0).TotalMinutes; /// /// Initializes a new instance of the class. /// - public SiteMapController(IContentQueryExecutor contentQueryExecutor, IWebPageUrlRetriever urlRetriever, IInfoProvider contentLanguageProvider) + public SiteMapController( + IContentQueryExecutor contentQueryExecutor, + IProgressiveCache progressiveCache, + IWebsiteChannelContext websiteChannelContext) { this.contentQueryExecutor = contentQueryExecutor; - this.urlRetriever = urlRetriever; - this.contentLanguageProvider = contentLanguageProvider; + this.progressiveCache = progressiveCache; + this.websiteChannelContext = websiteChannelContext; } @@ -42,76 +45,74 @@ public SiteMapController(IContentQueryExecutor contentQueryExecutor, IWebPageUrl [Route("/sitemap.xml")] public async Task Index() { - var options = new ContentQueryExecutionOptions - { - ForPreview = false, - IncludeSecuredItems = false - }; + var sitemapXml = await progressiveCache.LoadAsync(async _ => await GenerateSitemapXml(), GetCacheSettings()); - var relativeUrls = new List(); + return Content(sitemapXml, MediaTypeNames.Application.Xml); - foreach (var language in contentLanguageProvider.Get().OrderByDescending(i => i.ContentLanguageIsDefault)) + CacheSettings GetCacheSettings() => new(cacheMinutes, $"{nameof(SiteMapController)}|{nameof(Index)}") { - var builder = new ContentItemQueryBuilder().ForContentTypes(p => p.OfReusableSchema("SEOFields").ForWebsite()) - .InLanguage(language.ContentLanguageName, false) - .Parameters(p => p.Columns(nameof(IWebPageContentQueryDataContainer.WebPageItemID)) - .Where(w => w.WhereTrue(nameof(ISEOFields.SEOFieldsAllowSearchIndexing)))); - - var pageIdentifiers = await contentQueryExecutor.GetWebPageResult(builder, i => i.WebPageItemID, options, HttpContext.RequestAborted); - var languageUrls = await GetWebPageRelativeUrls(pageIdentifiers, language.ContentLanguageName); - - relativeUrls.AddRange(languageUrls); - } - - var absoluteUrls = GetAbsoluteUrls(relativeUrls); - var document = GetSitemap(absoluteUrls); - - return Content(document.OuterXml, XML_TYPE); + GetCacheDependency = () => CacheHelper.GetCacheDependency( + [ + // Since we can't detect only reusable field schema-based page changes, + // we need to respond to all changes in the content tree + $"webpageitem|bychannel|{websiteChannelContext.WebsiteChannelName}|childrenofpath|/", + // Since we can't detect only reusable field schema changes or + // additions or removals within a content type definition, + // we need to respond to all changes in content types + "cms.contenttype|all" + ]) + }; } - private async Task> GetWebPageRelativeUrls(IEnumerable pageIdentifiers, string languageName) + private async Task GenerateSitemapXml() { - var relativeUrls = new List(); - - foreach (var pageIdentifier in pageIdentifiers) + var options = new ContentQueryExecutionOptions { - var webPageUrl = await urlRetriever.Retrieve(pageIdentifier, languageName, false, HttpContext.RequestAborted); - relativeUrls.Add(webPageUrl.RelativePath.TrimStart('~')); - } - - return relativeUrls; - } - - - private IEnumerable GetAbsoluteUrls(IEnumerable relativeUrls) - { - var request = HttpContext.Request; + ForPreview = false, + IncludeSecuredItems = false + }; - return relativeUrls.Select(i => UriHelper.BuildAbsolute(request.Scheme, request.Host, path: i)).OrderBy(i => i); + var builder = new ContentItemQueryBuilder() + // Get all pages with the SEO fields schema in the current website + .ForContentTypes(p => p.OfReusableSchema(ISEOFields.REUSABLE_FIELD_SCHEMA_NAME).ForWebsite()) + .Parameters(p => + // Limit data to required columns + p.UrlPathColumns() + // Filter out pages that don't allow search indexing, + // the default value is true, so null values are considered as true as well + .Where(w => + w.WhereNull(nameof(ISEOFields.SEOFieldsAllowSearchIndexing)) + .Or().WhereTrue(nameof(ISEOFields.SEOFieldsAllowSearchIndexing)))); + + var languagePaths = await contentQueryExecutor.GetMappedWebPageResult(builder, options, HttpContext.RequestAborted); + + return BuildSitemap(languagePaths, HttpContext.Request); } - private static XmlDocument GetSitemap(IEnumerable urls) + private string BuildSitemap(IEnumerable pages, HttpRequest request) { - var document = new XmlDocument(); + var stringBuilder = new StringBuilder(); + using (var xmlWriter = XmlWriter.Create(stringBuilder, new XmlWriterSettings { Indent = true })) + { + xmlWriter.WriteStartDocument(); + xmlWriter.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9"); - var urlSet = document.CreateElement("urlset"); - urlSet.SetAttribute("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9"); + foreach (var page in pages) + { + var pageUrl = page.GetUrl(); - foreach (var url in urls) - { - var element = document.CreateElement("url"); - var location = document.CreateElement("loc"); - location.InnerText = url; + xmlWriter.WriteStartElement("url"); + xmlWriter.WriteElementString("loc", pageUrl.AbsoluteUrl); + xmlWriter.WriteEndElement(); + } - element.AppendChild(location); - urlSet.AppendChild(element); + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndDocument(); } - document.AppendChild(urlSet); - - return document; + return stringBuilder.ToString(); } } } diff --git a/examples/DancingGoat/DancingGoat.csproj b/examples/DancingGoat/DancingGoat.csproj index 7c20bde..865b240 100644 --- a/examples/DancingGoat/DancingGoat.csproj +++ b/examples/DancingGoat/DancingGoat.csproj @@ -30,6 +30,7 @@ +
\ No newline at end of file diff --git a/examples/DancingGoat/DancingGoatConstants.cs b/examples/DancingGoat/DancingGoatConstants.cs index 774b6b1..703c5ef 100644 --- a/examples/DancingGoat/DancingGoatConstants.cs +++ b/examples/DancingGoat/DancingGoatConstants.cs @@ -17,12 +17,30 @@ internal static class DancingGoatConstants public const string DEFAULT_ROUTE_WITHOUT_LANGUAGE_PREFIX_NAME = "defaultWithoutLanguagePrefix"; - public const string HOME_PAGE_PATH = "/Home"; + public const string WEBSITE_CHANNEL_NAME = "DancingGoatPages"; - public const string WEBSITE_CHANNEL_NAME = "DancingGoatPages"; + public const string COMMERCE_WORKSPACE_NAME = "DancingGoat.DancingGoatCommerce"; + + + public const string HOME_PAGE_TREE_PATH = "/Home"; + + + public const string SITE_NAVIGATION_MENU_TREE_PATH = "/Navigation_menu"; + + + public const string STORE_NAVIGATION_MENU_TREE_PATH = "/Navigation_menu/Store"; + + + public const string PRODUCTS_PAGE_TREE_PATH = "/Products"; + + + public const string STORE_PAGE_TREE_PATH = "/Store"; + + + public const string SHOPPING_CART_PAGE_TREE_PATH = "/Specials/ShoppingCart"; - public const string NAVIGATION_MENU_FOLDER_PATH = "/Navigation_menu"; + public const string CHECKOUT_PAGE_TREE_PATH = "/Specials/Checkout"; } } diff --git a/examples/DancingGoat/DancingGoatTaxonomyConstants.cs b/examples/DancingGoat/DancingGoatTaxonomyConstants.cs new file mode 100644 index 0000000..fba8dcb --- /dev/null +++ b/examples/DancingGoat/DancingGoatTaxonomyConstants.cs @@ -0,0 +1,22 @@ +namespace DancingGoat +{ + internal static class DancingGoatTaxonomyConstants + { + /// + /// Name of the product tag taxonomy. + /// + public const string PRODUCT_TAGS_TAXONOMY_NAME = "ProductTags"; + + + /// + /// Name of the product categories taxonomy. + /// + public const string PRODUCT_CATEGORIES_TAXONOMY_NAME = "ProductCategories"; + + + /// + /// Name of the product manufacturers taxonomy. + /// + public const string PRODUCT_MANUFACTURERS_TAXONOMY_NAME = "ProductManufacturers"; + } +} diff --git a/examples/DancingGoat/Data/Template.zip b/examples/DancingGoat/Data/Template.zip index 2c7f8f3..7841680 100644 Binary files a/examples/DancingGoat/Data/Template.zip and b/examples/DancingGoat/Data/Template.zip differ diff --git a/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor b/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor new file mode 100644 index 0000000..ce37a47 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor @@ -0,0 +1,9 @@ +@using Kentico.VisualBuilderComponents.Rcl.Components + +@namespace DancingGoat.EmailComponents + + + + + + diff --git a/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor.cs b/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor.cs new file mode 100644 index 0000000..3cd94a8 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Sections/DancingGoatFullWidthEmailSection.razor.cs @@ -0,0 +1,24 @@ +using DancingGoat.EmailComponents; + +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailSection( + identifier: DancingGoatFullWidthEmailSection.IDENTIFIER, + name: "Full-width section", + componentType: typeof(DancingGoatFullWidthEmailSection), + IconClass = "icon-l-header-text")] + +namespace DancingGoat.EmailComponents; + +/// +/// Basic section with one column. +/// +public partial class DancingGoatFullWidthEmailSection : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatFullWidthEmailSection)}"; +} diff --git a/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor b/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor new file mode 100644 index 0000000..af950a1 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor @@ -0,0 +1,12 @@ +@using Kentico.VisualBuilderComponents.Rcl.Components + +@namespace DancingGoat.EmailComponents + + + + + + + + + diff --git a/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor.cs b/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor.cs new file mode 100644 index 0000000..193fb24 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Sections/DancingGoatTwoColumnEmailSection.razor.cs @@ -0,0 +1,24 @@ +using DancingGoat.EmailComponents; + +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailSection( + identifier: DancingGoatTwoColumnEmailSection.IDENTIFIER, + name: "Two-column section", + componentType: typeof(DancingGoatTwoColumnEmailSection), + IconClass = "icon-l-cols-2")] + +namespace DancingGoat.EmailComponents; + +/// +/// Basic section with two columns. +/// +public partial class DancingGoatTwoColumnEmailSection : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatTwoColumnEmailSection)}"; +} diff --git a/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor b/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor new file mode 100644 index 0000000..1f598f2 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor @@ -0,0 +1,79 @@ +@using Kentico.VisualBuilderComponents.Rcl.Components + +@namespace DancingGoat.EmailComponents + + + + @EmailModel?.EmailSubject + + * { + font-family: Verdana, sans-serif; + line-height: 1.25; + } + h1, h2, h3, h4, a { color: rgba(132, 99, 49, 1); } + .footer-text { color: #846331; text-align: center; } + + + @EmailModel?.EmailPreviewText + + + + + + + + + + + + + + + + + + + + + + @if (EmailModel?.SocialPlatforms is not null) + { + foreach (var link in EmailModel.SocialPlatforms) + { + + + + } + } + + + + + + + Don't want to receive these emails? + Unsubscribe + + + + + + + + + © 2025 Dancing Goat. All rights reserved. + + + + + + + diff --git a/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor.cs b/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor.cs new file mode 100644 index 0000000..3478b4d --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Templates/DancingGoatEmailBuilderTemplate.razor.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; + +using CMS.EmailMarketing; + +using DancingGoat.EmailComponents; +using DancingGoat.Models; + +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailTemplate( + identifier: DancingGoatEmailBuilderTemplate.IDENTIFIER, + name: "Dancing Goat Regular Template (Email Builder)", + componentType: typeof(DancingGoatEmailBuilderTemplate), + ContentTypeNames = ["DancingGoat.BuilderEmail"]) +] + +namespace DancingGoat.EmailComponents; + +/// +/// The email builder template component. +/// +public partial class DancingGoatEmailBuilderTemplate : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatEmailBuilderTemplate)}"; + + + private BuilderEmail EmailModel { get; set; } + + + private EmailRecipientContext EmailRecipientContext { get; set; } + + + [Inject] + private IEmailContextAccessor EmailContextAccessor { get; set; } + + + [Inject] + private IEmailRecipientContextAccessor EmailRecipientContextAccessor { get; set; } + + + /// + protected override async Task OnInitializedAsync() + { + var context = EmailContextAccessor.GetContext(); + EmailModel = await context.GetEmail(); + + EmailRecipientContext = EmailRecipientContextAccessor.GetContext(); + } +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor new file mode 100644 index 0000000..98425b6 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor @@ -0,0 +1,24 @@ +@using DancingGoat.EmailComponents.Enums + +@namespace DancingGoat.EmailComponents + +@if (Properties.ButtonType == nameof(DancingGoatButtonType.Button)) +{ + + @Properties.Text + +} +else +{ + + + @Properties.Text + + +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor.cs b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor.cs new file mode 100644 index 0000000..b98d201 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidget.razor.cs @@ -0,0 +1,34 @@ +using DancingGoat.EmailComponents; + +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailWidget( + identifier: DancingGoatButtonWidget.IDENTIFIER, + name: "Button", + componentType: typeof(DancingGoatButtonWidget), + PropertiesType = typeof(DancingGoatButtonWidgetProperties), + IconClass = "icon-arrow-right-top-square", + Description = "Displays a button that opens a specified URL when clicked." + )] + +namespace DancingGoat.EmailComponents; + +/// +/// Button widget component. +/// +public partial class DancingGoatButtonWidget : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatButtonWidget)}"; + + + /// + /// The widget properties. + /// + [Parameter] + public DancingGoatButtonWidgetProperties Properties { get; set; } = null!; +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidgetProperties.cs b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidgetProperties.cs new file mode 100644 index 0000000..f43a8fe --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ButtonWidget/DancingGoatButtonWidgetProperties.cs @@ -0,0 +1,54 @@ +using DancingGoat.EmailComponents.Enums; + +using Kentico.EmailBuilder.Web.Mvc; +using Kentico.Xperience.Admin.Base.FormAnnotations; +using Kentico.Xperience.Admin.Websites.FormAnnotations; + +namespace DancingGoat.EmailComponents; + +/// +/// Configurable properties of the . +/// +public class DancingGoatButtonWidgetProperties : IEmailWidgetProperties +{ + /// + /// The button text. + /// + [TextInputComponent( + Label = "Button text", + Order = 1, + ExplanationText = "Enter the text displayed as the button's caption.")] + public string Text { get; set; } = string.Empty; + + + /// + /// The URL linked by button. + /// + [UrlSelectorComponent(Label = "Link URL", + Order = 2)] + public string Url { get; set; } + + + /// + /// The button HTML element type. + /// + [DropDownComponent( + Label = "Button type", + Order = 3, + ExplanationText = "Choose how the button is displayed.", + Options = $"{nameof(DancingGoatButtonType.Button)};Button\r\n{nameof(DancingGoatButtonType.Link)};Link", + OptionsValueSeparator = ";")] + public string ButtonType { get; set; } = nameof(DancingGoatButtonType.Button); + + + /// + /// The horizontal alignment of the button. + /// + [DropDownComponent( + Label = "Alignment", + Order = 4, + ExplanationText = "Select how you want to position the button", + Options = $"{nameof(DancingGoatHorizontalAlignment.Left)};Left\r\n{nameof(DancingGoatHorizontalAlignment.Center)};Center\r\n{nameof(DancingGoatHorizontalAlignment.Right)};Right", + OptionsValueSeparator = ";")] + public string ButtonHorizontalAlignment { get; set; } = nameof(DancingGoatHorizontalAlignment.Center); +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatButtonType.cs b/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatButtonType.cs new file mode 100644 index 0000000..f660606 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatButtonType.cs @@ -0,0 +1,18 @@ +namespace DancingGoat.EmailComponents.Enums; + +/// +/// The type of HTML element rendered by button widget. +/// +public enum DancingGoatButtonType +{ + /// + /// <button /> HTML element. + /// + Button, + + + /// + /// <a /> HTML element. + /// + Link +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatHorizontalAlignment.cs b/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatHorizontalAlignment.cs new file mode 100644 index 0000000..fb1e9e0 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/Enums/DancingGoatHorizontalAlignment.cs @@ -0,0 +1,24 @@ +namespace DancingGoat.EmailComponents.Enums; + +/// +/// The horizontal alignment. +/// +public enum DancingGoatHorizontalAlignment +{ + /// + /// Left. + /// + Left, + + + /// + /// Center. + /// + Center, + + + /// + /// Right. + /// + Right +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor new file mode 100644 index 0000000..e846b2d --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor @@ -0,0 +1,3 @@ +@namespace DancingGoat.EmailComponents + + diff --git a/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor.cs b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor.cs new file mode 100644 index 0000000..576aeb5 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidget.razor.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using DancingGoat.EmailComponents; +using DancingGoat.Models; + +using Kentico.Content.Web.Mvc; +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailWidget( + identifier: DancingGoatImageWidget.IDENTIFIER, + name: "Image", + componentType: typeof(DancingGoatImageWidget), + PropertiesType = typeof(DancingGoatImageWidgetProperties), + IconClass = "icon-picture", + Description = "Displays an image, which can be selected from images stored as assets in Content hub." + )] + +namespace DancingGoat.EmailComponents; + +/// +/// Image widget component. +/// +public partial class DancingGoatImageWidget : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatImageWidget)}"; + + + /// + /// The URL of the image. + /// + private string Url { get; set; } = string.Empty; + + + /// + /// The alternative text for the image. + /// + private string AlternativeText { get; set; } = string.Empty; + + + /// + /// The email context accessor used to retrieve the current email context. + /// + [Inject] + private IEmailContextAccessor EmailContextAccessor { get; set; } + + + /// + /// The content retriever used to retrieve content items. + /// + [Inject] + private IContentRetriever ContentRetriever { get; set; } + + + /// + /// The widget properties. + /// + [Parameter] + public DancingGoatImageWidgetProperties Properties { get; set; } = null!; + + + /// + protected override async Task OnInitializedAsync() + { + await BindProperties(); + } + + + private async Task BindProperties() + { + var itemGuid = Properties.Assets?.Select(i => i.Identifier).FirstOrDefault(); + + if (!itemGuid.HasValue || itemGuid.Value == Guid.Empty) + { + return; + } + + var languageName = EmailContextAccessor.GetContext().LanguageName; + + var parameters = new RetrieveContentParameters + { + LanguageName = languageName, + IsForPreview = false, + }; + + var image = (await ContentRetriever.RetrieveContentByGuids( + [itemGuid.Value], + parameters, + query => query.TopN(1), + new RetrievalCacheSettings( + "TopN_1", + useSlidingExpiration: true, + cacheExpiration: TimeSpan.FromMinutes(1)))) + .FirstOrDefault(); + + Url = image?.ImageFile?.Url ?? string.Empty; + AlternativeText = image?.ImageShortDescription ?? string.Empty; + } +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidgetProperties.cs b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidgetProperties.cs new file mode 100644 index 0000000..cf36b75 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/ImageWidget/DancingGoatImageWidgetProperties.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +using CMS.ContentEngine; + +using DancingGoat.EmailComponents.Enums; +using DancingGoat.Models; + +using Kentico.EmailBuilder.Web.Mvc; +using Kentico.Xperience.Admin.Base.FormAnnotations; + +namespace DancingGoat.EmailComponents; + +/// +/// Configurable properties of the . +/// +public class DancingGoatImageWidgetProperties : IEmailWidgetProperties +{ + /// + /// The image. + /// + [ContentItemSelectorComponent( + Image.CONTENT_TYPE_NAME, + Order = 1, + Label = "Image", + ExplanationText = "Select the image from assets stored in the Content hub.", + MaximumItems = 1)] + public IEnumerable Assets { get; set; } = []; + + + /// + /// The horizontal alignment of the button. + /// + [DropDownComponent( + Label = "Alignment", + Order = 2, + ExplanationText = "Allows you to set the width of the image in pixels.", + Options = $"{nameof(DancingGoatHorizontalAlignment.Left)};Left\r\n{nameof(DancingGoatHorizontalAlignment.Center)};Center\r\n{nameof(DancingGoatHorizontalAlignment.Right)};Right", + OptionsValueSeparator = ";")] + public string Alignment { get; set; } = nameof(DancingGoatHorizontalAlignment.Center); + + + /// + /// The image width. + /// + [NumberInputComponent( + Label = "Width", + Order = 3, + ExplanationText = "Allows you to set the width of the image in pixels.")] + public int? Width { get; set; } + + + /// + /// The image width. + /// + [NumberInputComponent( + Label = "Height", + Order = 4, + ExplanationText = "Allows you to set the height of the image in pixels.")] + public int? Height { get; set; } +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor new file mode 100644 index 0000000..5082eb1 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor @@ -0,0 +1,17 @@ +@using Kentico.EmailBuilder.Web.Mvc +@using Kentico.VisualBuilderComponents.Rcl.Components + +@namespace DancingGoat.EmailComponents + +@if (EmailContext.BuilderMode == EmailBuilderMode.Edit) +{ + + + +} +else +{ + + @((MarkupString)Properties.Text) + +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor.cs b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor.cs new file mode 100644 index 0000000..434d611 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidget.razor.cs @@ -0,0 +1,50 @@ +using DancingGoat.EmailComponents; + +using Kentico.EmailBuilder.Web.Mvc; + +using Microsoft.AspNetCore.Components; + +[assembly: RegisterEmailWidget( + identifier: DancingGoatTextWidget.IDENTIFIER, + name: "Text", + componentType: typeof(DancingGoatTextWidget), + PropertiesType = typeof(DancingGoatTextWidgetProperties), + IconClass = "icon-l-header-text", + Description = "Allows add and format text content." + )] + +namespace DancingGoat.EmailComponents; + +/// +/// Text widget component. +/// +public partial class DancingGoatTextWidget : ComponentBase +{ + /// + /// The component identifier. + /// + public const string IDENTIFIER = $"DancingGoat.{nameof(DancingGoatTextWidget)}"; + + + private EmailContext emailContext; + + + /// + /// The widget properties. + /// + [Parameter] + public DancingGoatTextWidgetProperties Properties { get; set; } = null!; + + + /// + /// Gets or sets the email context accessor service. + /// + [Inject] + private IEmailContextAccessor EmailContextAccessor { get; set; } = null!; + + + /// + /// Gets the current email context. + /// + private EmailContext EmailContext => emailContext ??= EmailContextAccessor.GetContext(); +} diff --git a/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidgetProperties.cs b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidgetProperties.cs new file mode 100644 index 0000000..cd1bbf3 --- /dev/null +++ b/examples/DancingGoat/EmailComponents/Widgets/TextWidget/DancingGoatTextWidgetProperties.cs @@ -0,0 +1,17 @@ +using CMS.ContentEngine; + +using Kentico.EmailBuilder.Web.Mvc; + +namespace DancingGoat.EmailComponents; + +/// +/// Configurable properties of the . +/// +public class DancingGoatTextWidgetProperties : IEmailWidgetProperties +{ + /// + /// The widget content. + /// + [TrackContentItemReference(typeof(ContentItemReferenceExtractor))] + public string Text { get; set; } = string.Empty; +} diff --git a/examples/DancingGoat/Helpers/ExpressionExtensions.cs b/examples/DancingGoat/Helpers/ExpressionExtensions.cs new file mode 100644 index 0000000..c5f1040 --- /dev/null +++ b/examples/DancingGoat/Helpers/ExpressionExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq.Expressions; + +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace DancingGoat.Helpers; + +internal static class ExpressionExtensions +{ + private static readonly ModelExpressionProvider ModelExpressionProvider = new ModelExpressionProvider(new EmptyModelMetadataProvider()); + + /// + /// Returns the expression text for the specified expression. + /// + public static string GetExpressionText(this Expression> expression) + { + return ModelExpressionProvider.GetExpressionText(expression); + } +} diff --git a/examples/DancingGoat/Helpers/Generators/ForbiddenPasswordGenerator.cs b/examples/DancingGoat/Helpers/Generators/ForbiddenPasswordGenerator.cs new file mode 100644 index 0000000..044607e --- /dev/null +++ b/examples/DancingGoat/Helpers/Generators/ForbiddenPasswordGenerator.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DancingGoat.Helpers.Generators +{ + /// + /// Contains methods for generating forbidden passwords. + /// + public static class ForbiddenPasswordGenerator + { + private static readonly List SpecialChars = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '.', '?', '-', '_', '=', '+', '[', ']', '{', '}', '\\', '|', ';', ':', '\'', '"', ',', '<', '>', '/', '~', '`']; + private static readonly List Numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; + + /// + /// Generates forbidden passwords based on company-specific keywords and specific number combinations. + /// + /// + /// The forbidden passwords do not include keywords without special characters and numbers, since these are already blocked by the default password policy. + /// + /// Company specific keywords + /// Specific number combinations + public static HashSet Generate(List companySpecificKeywords, List specificNumberCombinations) + { + var numbers = Numbers.Concat(specificNumberCombinations); + + var forbiddenPasswords = + from keyword in companySpecificKeywords + from specialChar in SpecialChars + from number in numbers + from forbiddenPassword in new[] + { + keyword + specialChar + number, + keyword + number + specialChar, + specialChar + keyword + number, + number + keyword + specialChar + } + select forbiddenPassword; + + return new HashSet(forbiddenPasswords); + } + } +} diff --git a/examples/DancingGoat/Helpers/HtmlHelperExtensions.cs b/examples/DancingGoat/Helpers/HtmlHelperExtensions.cs new file mode 100644 index 0000000..9ba2ca9 --- /dev/null +++ b/examples/DancingGoat/Helpers/HtmlHelperExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq.Expressions; + +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace DancingGoat.Helpers; + +internal static class HtmlHelperExtensions +{ + /// + /// Returns an HTML input element with a label and validation fields for each property in the object that is represented by the expression. + /// + /// The type of the model. + /// The type of the value. + /// The HTML helper instance that this method extends. + /// An expression that identifies the object that contains the displayed properties. + /// An explanation text describing usage of the rendered field. + /// Indicates that field has to be disabled. + public static IHtmlContent ValidatedEditorFor(this IHtmlHelper html, Expression> expression, LocalizedHtmlString explanationText = null, bool disabled = false) + { + var label = html.LabelFor(expression); + + var additionalViewData = new { htmlAttributes = new { data_storage = $"{GetModelName(html)}_{expression.GetExpressionText()}" } }; + var disabledAdditionalViewData = new { htmlAttributes = new { disabled = "disabled" } }; + + var editor = html.EditorFor(expression, !disabled ? additionalViewData : disabledAdditionalViewData); + var message = html.ValidationMessageFor(expression); + IHtmlContent explanationTextHtml = HtmlString.Empty; + + if (explanationText != null) + { + var explanationDiv = new TagBuilder("div"); + explanationDiv.AddCssClass("explanation-text"); + explanationDiv.InnerHtml.AppendHtml(explanationText); + explanationDiv.RenderEndTag(); + explanationTextHtml = explanationDiv; + } + + var generatedHtml = new HtmlContentBuilder().AppendFormat(@" +
+
{0}
+
{1} + {2} +
+
{3}
+
", label, editor, explanationTextHtml, message); + + return generatedHtml; + } + + + private static string GetModelName(IHtmlHelper html) + { + return html.GetType().GenericTypeArguments[0].Name.Replace("ViewModel", ""); + } +} diff --git a/examples/DancingGoat/Helpers/TagHelpers/ActiveProductCategoryLinkTagHelper.cs b/examples/DancingGoat/Helpers/TagHelpers/ActiveProductCategoryLinkTagHelper.cs new file mode 100644 index 0000000..45f43da --- /dev/null +++ b/examples/DancingGoat/Helpers/TagHelpers/ActiveProductCategoryLinkTagHelper.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace DancingGoat.Helpers; + +[HtmlTargetElement("a", Attributes = "asp-active")] +public class ActiveProductCategoryLinkTagHelper : TagHelper +{ + private readonly IUrlHelperFactory urlHelperFactory; + private readonly IActionContextAccessor actionContextAccessor; + + + [HtmlAttributeName("asp-active")] + public string ActiveHref { get; set; } + + + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } + + + public ActiveProductCategoryLinkTagHelper(IUrlHelperFactory urlHelperFactory, IActionContextAccessor actionContextAccessor) + { + this.urlHelperFactory = urlHelperFactory; + this.actionContextAccessor = actionContextAccessor; + } + + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var actionContext = actionContextAccessor.ActionContext; + var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); + + var currentPath = ViewContext.HttpContext.Request.Path.Value?.ToLowerInvariant(); + + // Resolve ActiveHref using UrlHelper + var activeHrefResolved = urlHelper.Content(ActiveHref); + + if (!string.IsNullOrEmpty(currentPath) && !string.IsNullOrEmpty(activeHrefResolved) && + (currentPath == activeHrefResolved || currentPath.StartsWith(activeHrefResolved))) + { + var existingClass = output.Attributes["class"]?.Value?.ToString() ?? ""; + output.Attributes.SetAttribute("class", $"{existingClass} active".Trim()); + } + + // Remove asp-active attribute so it doesn't appear in the rendered HTML + output.Attributes.RemoveAll("asp-active"); + } +} diff --git a/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs b/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs index fc9bfd5..304278a 100644 --- a/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs +++ b/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs @@ -22,7 +22,7 @@ public class LanguageLinkTagHelper : TagHelper private readonly IHtmlGenerator htmlGenerator; private readonly IWebPageUrlRetriever webPageUrlRetriever; private readonly IPreferredLanguageRetriever currentLanguageRetriever; - private readonly ICurrentWebsiteChannelPrimaryLanguageRetriever websiteChannelPrimaryLanguageRetriever; + private readonly CurrentWebsiteChannelPrimaryLanguageRetriever websiteChannelPrimaryLanguageRetriever; public string LinkText { get; set; } @@ -42,7 +42,7 @@ public LanguageLinkTagHelper( IHtmlGenerator htmlGenerator, IWebPageUrlRetriever webPageUrlRetriever, IPreferredLanguageRetriever currentLanguageRetriever, - ICurrentWebsiteChannelPrimaryLanguageRetriever websiteChannelPrimaryLanguageRetriever) + CurrentWebsiteChannelPrimaryLanguageRetriever websiteChannelPrimaryLanguageRetriever) { this.httpContextAccessor = httpContextAccessor; this.pageDataContextRetriever = pageDataContextRetriever; diff --git a/examples/DancingGoat/Helpers/TagHelpers/PriceTagHelper.cs b/examples/DancingGoat/Helpers/TagHelpers/PriceTagHelper.cs new file mode 100644 index 0000000..79b92e3 --- /dev/null +++ b/examples/DancingGoat/Helpers/TagHelpers/PriceTagHelper.cs @@ -0,0 +1,35 @@ +using CMS.Commerce; + +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace DancingGoat.Helpers; + +[HtmlTargetElement("price")] +public class PriceTagHelper : TagHelper +{ +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + private readonly IPriceFormatter priceFormatter; + + + public PriceTagHelper(IPriceFormatter priceFormatter) + { + this.priceFormatter = priceFormatter; + } + + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "price"; + var content = output.GetChildContentAsync().Result.GetContent(); + + if (decimal.TryParse(content, out var amount)) + { + output.Content.SetContent(priceFormatter.Format(amount, new PriceFormatConfiguration())); + } + else + { + output.Content.SetContent(content); + } + } +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +} diff --git a/examples/DancingGoat/Models/Email/BuilderEmail.generated.cs b/examples/DancingGoat/Models/Email/BuilderEmail.generated.cs new file mode 100644 index 0000000..0d92de0 --- /dev/null +++ b/examples/DancingGoat/Models/Email/BuilderEmail.generated.cs @@ -0,0 +1,61 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; +using CMS.EmailLibrary; + +namespace DancingGoat.Models +{ + /// + /// Represents an email of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class BuilderEmail : IEmailFieldsSource + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.BuilderEmail"; + + + /// + /// Represents system properties for an email item. + /// + [SystemField] + public EmailFields SystemFields { get; set; } + + + /// + /// EmailSubject. + /// + public string EmailSubject { get; set; } + + + /// + /// EmailPreviewText. + /// + public string EmailPreviewText { get; set; } + + + /// + /// BannerLogo. + /// + public IEnumerable BannerLogo { get; set; } + + + /// + /// SocialPlatforms. + /// + public IEnumerable SocialPlatforms { get; set; } + } +} diff --git a/examples/DancingGoat/Models/Reusable/Cafe/Cafe.generated.cs b/examples/DancingGoat/Models/Reusable/Cafe/Cafe.generated.cs index 46ba1ef..a284528 100644 --- a/examples/DancingGoat/Models/Reusable/Cafe/Cafe.generated.cs +++ b/examples/DancingGoat/Models/Reusable/Cafe/Cafe.generated.cs @@ -91,6 +91,12 @@ public partial class Cafe : IContentItemFieldsSource /// /// CafeCuppingOffer. /// - public IEnumerable CafeCuppingOffer { get; set; } + public IEnumerable CafeCuppingOffer { get; set; } + + + /// + /// CafePromotion. + /// + public IEnumerable CafePromotion { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs b/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs deleted file mode 100644 index 2bb02a5..0000000 --- a/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of cafes. - /// - public partial class CafeRepository : ContentRepositoryBase - { - private readonly ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever; - - - public CafeRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IProgressiveCache cache, - ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; - } - - /// - /// Returns an enumerable collection of cafes. - /// - public async Task> GetCafes(int count, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(count, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(CafeRepository), nameof(GetCafes), count, languageName); - - return await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(int count, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(Cafe.CONTENT_TYPE_NAME, - config => config - .WithLinkedItems(1) - .TopN(count)) - .InLanguage(languageName); - } - - - private async Task> GetDependencyCacheKeys(IEnumerable cafes, CancellationToken cancellationToken) - { - var cafeIds = cafes.Select(cafe => cafe.SystemFields.ContentItemID); - var dependencyCacheKeys = (await linkedItemsDependencyRetriever.Get(cafeIds, 1, cancellationToken)) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "contentitem", "bycontenttype", Cafe.CONTENT_TYPE_NAME }, false)); - - return dependencyCacheKeys; - } - } -} diff --git a/examples/DancingGoat/Models/Reusable/Contact/Contact.generated.cs b/examples/DancingGoat/Models/Reusable/Contact/Contact.generated.cs index 541e1d6..fe01fa6 100644 --- a/examples/DancingGoat/Models/Reusable/Contact/Contact.generated.cs +++ b/examples/DancingGoat/Models/Reusable/Contact/Contact.generated.cs @@ -58,6 +58,12 @@ public partial class Contact : IContentItemFieldsSource public string ContactCountry { get; set; } + /// + /// ContactUSState. + /// + public string ContactUSState { get; set; } + + /// /// ContactZipCode. /// diff --git a/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs b/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs deleted file mode 100644 index 96f9025..0000000 --- a/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of contact information. - /// - public class ContactRepository : ContentRepositoryBase - { - public ContactRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns the first content item. - /// - public async Task GetContact(string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, languageName, nameof(Contact)); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(Contact.CONTENT_TYPE_NAME, config => config.TopN(1)) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable contacts, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var contact = contacts.FirstOrDefault(); - - if (contact != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "contentitem", "byid", contact.SystemFields.ContentItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs b/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs index fb7ce36..0dde880 100644 --- a/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs @@ -26,7 +26,7 @@ public static EventViewModel GetViewModel(Event eventContentItem) eventContentItem.EventPromoText, eventContentItem.EventDate, cafe?.CafeName, - cafe?.CafeCuppingOffer.Select(coffee => coffee.ProductFieldsName) + cafe?.CafeCuppingOffer.Select(coffee => coffee.ProductFieldName) ); } } diff --git a/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs b/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs deleted file mode 100644 index 2852d46..0000000 --- a/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - public class ImageRepository : ContentRepositoryBase - { - public ImageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns content item. - /// - public async Task GetImage(Guid imageGuid, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(imageGuid, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ImageRepository), nameof(GetImage), languageName, imageGuid); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(Guid imageGuid, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(Image.CONTENT_TYPE_NAME, - config => config - .TopN(1) - .Where(where => where.WhereEquals(nameof(IContentQueryDataContainer.ContentItemGUID), imageGuid))) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable images, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var image = images.FirstOrDefault(); - - if (image != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "contentitem", "byid", image.SystemFields.ContentItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/Reusable/Product/ProductRepository.cs b/examples/DancingGoat/Models/Reusable/Product/ProductRepository.cs deleted file mode 100644 index ff114df..0000000 --- a/examples/DancingGoat/Models/Reusable/Product/ProductRepository.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.DataEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of product pages. - /// - public class ProductRepository : ContentRepositoryBase - { - private const string COFFEE_PROCESSING = "CoffeeProcessing"; - private const string COFFEE_TASTES = "CoffeeTastes"; - private const string GRINDER_MANUFACTURER = "GrinderManufacturer"; - private const string GRINDER_TYPE = "GrinderType"; - - - private readonly ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever; - private readonly IInfoProvider taxonomyInfoProvider; - - - /// - /// Initializes new instance of . - /// - public ProductRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IProgressiveCache cache, - ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever, - IInfoProvider taxonomyInfoProvider) - : base(websiteChannelContext, executor, cache) - { - this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; - this.taxonomyInfoProvider = taxonomyInfoProvider; - } - - - /// - /// Returns list of content items. - /// - public async Task> GetProducts(string languageName, IDictionary filter, bool includeSecuredItems = true, CancellationToken cancellationToken = default) - { - var queryBuilder = await GetQueryBuilder(languageName, filter: filter); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = includeSecuredItems - }; - - var filterCacheItemNameParts = filter.Values.Where(value => value != null && value.Tags != null).SelectMany(value => value.Tags.Where(tag => tag.IsChecked)).Select(id => id.Value.ToString()).Join("|"); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, languageName, includeSecuredItems, nameof(IProductFields), filterCacheItemNameParts); - - return await GetCachedQueryResult(queryBuilder, options, cacheSettings, (_, _) => GetDependencyCacheKeys(languageName), cancellationToken); - } - - - /// - /// Returns list of content items. - /// - public async Task> GetProducts(ICollection productGuids, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = await GetQueryBuilder(languageName, productGuids); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, languageName, nameof(IProductFields), productGuids.Select(guid => guid.ToString()).Join("|")); - - return await GetCachedQueryResult(queryBuilder, new ContentQueryExecutionOptions(), cacheSettings, (_, _) => GetDependencyCacheKeys(languageName, productGuids), cancellationToken); - } - - - private static async Task GetQueryBuilder(string languageName, IEnumerable productGuids = null, IDictionary filter = null) - { - var baseBuilder = new ContentItemQueryBuilder().ForContentTypes(ct => - { - ct.OfReusableSchema(IProductFields.REUSABLE_FIELD_SCHEMA_NAME) - .WithContentTypeFields() - .WithLinkedItems(1); - }).InLanguage(languageName); - - if (productGuids != null) - { - baseBuilder.Parameters(query => query.Where(where => where.WhereIn(nameof(IContentQueryDataContainer.ContentItemGUID), productGuids))); - } - - if (filter == null || !filter.Any()) - { - return baseBuilder; - } - - var coffeeProcessingTags = await GetSelectedTags(filter, COFFEE_PROCESSING); - var coffeeTastesTags = await GetSelectedTags(filter, COFFEE_TASTES); - var grinderManufacturerTags = await GetSelectedTags(filter, GRINDER_MANUFACTURER); - var grinderTypeTags = await GetSelectedTags(filter, GRINDER_TYPE); - - return baseBuilder - .Parameters(query => query.Where(where => where - .Where(coffeeWhere => coffeeWhere - .WhereContainsTags(nameof(Coffee.CoffeeProcessing), coffeeProcessingTags) - .WhereContainsTags(nameof(Coffee.CoffeeTastes), coffeeTastesTags)) - .Where(grinderWhere => grinderWhere - .WhereContainsTags(nameof(Grinder.GrinderManufacturer), grinderManufacturerTags) - .WhereContainsTags(nameof(Grinder.GrinderType), grinderTypeTags)) - )); - } - - - private static async Task GetSelectedTags(IDictionary filter, string taxonomyName) - { - if (filter.TryGetValue(taxonomyName, out var taxonomy)) - { - return await taxonomy.GetSelectedTags(); - } - - return null; - } - - - private async Task> GetDependencyCacheKeys(string languageName, ICollection productGuids = null) - { - var dependencyCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID), - CacheHelper.GetCacheItemName(null, "contentitem", "bycontenttype", Coffee.CONTENT_TYPE_NAME, languageName), - CacheHelper.GetCacheItemName(null, "contentitem", "bycontenttype", Grinder.CONTENT_TYPE_NAME, languageName), - await GetTaxonomyTagsCacheDependencyKey(COFFEE_PROCESSING), - await GetTaxonomyTagsCacheDependencyKey(COFFEE_TASTES), - await GetTaxonomyTagsCacheDependencyKey(GRINDER_MANUFACTURER), - await GetTaxonomyTagsCacheDependencyKey(GRINDER_TYPE) - }; - GetProductPageDependencies(productGuids, dependencyCacheKeys); - - return dependencyCacheKeys; - } - - - private static void GetProductPageDependencies(ICollection productGuids, HashSet dependencyCacheKeys) - { - if (productGuids == null || !productGuids.Any()) - { - return; - } - - foreach (var guid in productGuids) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "contentitem", "byguid", guid.ToString() }, false)); - } - } - - - private async Task GetTaxonomyTagsCacheDependencyKey(string taxonomyName) - { - var taxonomyID = (await taxonomyInfoProvider.GetAsync(taxonomyName)).TaxonomyID; - return CacheHelper.GetCacheItemName(null, TaxonomyInfo.OBJECT_TYPE, "byid", taxonomyID, "children"); - } - } -} diff --git a/examples/DancingGoat/Models/Reusable/ProductAccessory/ProductAccessory.generated.cs b/examples/DancingGoat/Models/Reusable/ProductAccessory/ProductAccessory.generated.cs new file mode 100644 index 0000000..9c6b1c1 --- /dev/null +++ b/examples/DancingGoat/Models/Reusable/ProductAccessory/ProductAccessory.generated.cs @@ -0,0 +1,84 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; + +namespace DancingGoat.Models +{ + /// + /// Represents a content item of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ProductAccessory : IContentItemFieldsSource, IProductFields, IProductSKU, IProductManufacturer + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductAccessory"; + + + /// + /// Represents system properties for a content item. + /// + [SystemField] + public ContentItemFields SystemFields { get; set; } + + + /// + /// ProductFieldName. + /// + public string ProductFieldName { get; set; } + + + /// + /// ProductFieldDescription. + /// + public string ProductFieldDescription { get; set; } + + + /// + /// ProductFieldImage. + /// + public IEnumerable ProductFieldImage { get; set; } + + + /// + /// ProductFieldPrice. + /// + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } + + + /// + /// ProductSKUCode. + /// + public string ProductSKUCode { get; set; } + + + /// + /// ProductManufacturerTag. + /// + public IEnumerable ProductManufacturerTag { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/ProductBrewer/ProductBrewer.generated.cs b/examples/DancingGoat/Models/Reusable/ProductBrewer/ProductBrewer.generated.cs new file mode 100644 index 0000000..5abf71c --- /dev/null +++ b/examples/DancingGoat/Models/Reusable/ProductBrewer/ProductBrewer.generated.cs @@ -0,0 +1,84 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; + +namespace DancingGoat.Models +{ + /// + /// Represents a content item of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ProductBrewer : IContentItemFieldsSource, IProductFields, IProductSKU, IProductManufacturer + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductBrewer"; + + + /// + /// Represents system properties for a content item. + /// + [SystemField] + public ContentItemFields SystemFields { get; set; } + + + /// + /// ProductFieldName. + /// + public string ProductFieldName { get; set; } + + + /// + /// ProductFieldDescription. + /// + public string ProductFieldDescription { get; set; } + + + /// + /// ProductFieldImage. + /// + public IEnumerable ProductFieldImage { get; set; } + + + /// + /// ProductFieldPrice. + /// + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } + + + /// + /// ProductSKUCode. + /// + public string ProductSKUCode { get; set; } + + + /// + /// ProductManufacturerTag. + /// + public IEnumerable ProductManufacturerTag { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Coffee/Coffee.generated.cs b/examples/DancingGoat/Models/Reusable/ProductCoffee/ProductCoffee.generated.cs similarity index 58% rename from examples/DancingGoat/Models/Reusable/Coffee/Coffee.generated.cs rename to examples/DancingGoat/Models/Reusable/ProductCoffee/ProductCoffee.generated.cs index 4271000..b83c426 100644 --- a/examples/DancingGoat/Models/Reusable/Coffee/Coffee.generated.cs +++ b/examples/DancingGoat/Models/Reusable/ProductCoffee/ProductCoffee.generated.cs @@ -16,15 +16,15 @@ namespace DancingGoat.Models { /// - /// Represents a content item of type . + /// Represents a content item of type . /// [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] - public partial class Coffee : IContentItemFieldsSource, IProductFields + public partial class ProductCoffee : IContentItemFieldsSource, IProductFields, IProductSKU { /// /// Code name of the content type. /// - public const string CONTENT_TYPE_NAME = "DancingGoat.Coffee"; + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductCoffee"; /// @@ -47,26 +47,44 @@ public partial class Coffee : IContentItemFieldsSource, IProductFields /// - /// ProductFieldsName. + /// ProductFieldName. /// - public string ProductFieldsName { get; set; } + public string ProductFieldName { get; set; } /// - /// ProductFieldsDescription. + /// ProductFieldDescription. /// - public string ProductFieldsDescription { get; set; } + public string ProductFieldDescription { get; set; } /// - /// ProductFieldsShortDescription. + /// ProductFieldImage. /// - public string ProductFieldsShortDescription { get; set; } + public IEnumerable ProductFieldImage { get; set; } /// - /// ProductFieldsImage. + /// ProductFieldPrice. /// - public IEnumerable ProductFieldsImage { get; set; } + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } + + + /// + /// ProductSKUCode. + /// + public string ProductSKUCode { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/ProductGrinder/ProductGrinder.generated.cs b/examples/DancingGoat/Models/Reusable/ProductGrinder/ProductGrinder.generated.cs new file mode 100644 index 0000000..44c61a4 --- /dev/null +++ b/examples/DancingGoat/Models/Reusable/ProductGrinder/ProductGrinder.generated.cs @@ -0,0 +1,96 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; + +namespace DancingGoat.Models +{ + /// + /// Represents a content item of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ProductGrinder : IContentItemFieldsSource, IProductFields, IProductSKU, IProductManufacturer + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductGrinder"; + + + /// + /// Represents system properties for a content item. + /// + [SystemField] + public ContentItemFields SystemFields { get; set; } + + + /// + /// GrinderType. + /// + public string GrinderType { get; set; } + + + /// + /// GrinderPower. + /// + public decimal GrinderPower { get; set; } + + + /// + /// ProductFieldName. + /// + public string ProductFieldName { get; set; } + + + /// + /// ProductFieldDescription. + /// + public string ProductFieldDescription { get; set; } + + + /// + /// ProductFieldImage. + /// + public IEnumerable ProductFieldImage { get; set; } + + + /// + /// ProductFieldPrice. + /// + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } + + + /// + /// ProductSKUCode. + /// + public string ProductSKUCode { get; set; } + + + /// + /// ProductManufacturerTag. + /// + public IEnumerable ProductManufacturerTag { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/ProductTemplateAlphaSize/ProductTemplateAlphaSize.generated.cs b/examples/DancingGoat/Models/Reusable/ProductTemplateAlphaSize/ProductTemplateAlphaSize.generated.cs new file mode 100644 index 0000000..22b813f --- /dev/null +++ b/examples/DancingGoat/Models/Reusable/ProductTemplateAlphaSize/ProductTemplateAlphaSize.generated.cs @@ -0,0 +1,84 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; + +namespace DancingGoat.Models +{ + /// + /// Represents a content item of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ProductTemplateAlphaSize : IContentItemFieldsSource, IProductFields, IProductManufacturer + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductTemplateAlphaSize"; + + + /// + /// Represents system properties for a content item. + /// + [SystemField] + public ContentItemFields SystemFields { get; set; } + + + /// + /// ProductVariants. + /// + public IEnumerable ProductVariants { get; set; } + + + /// + /// ProductFieldName. + /// + public string ProductFieldName { get; set; } + + + /// + /// ProductFieldDescription. + /// + public string ProductFieldDescription { get; set; } + + + /// + /// ProductFieldImage. + /// + public IEnumerable ProductFieldImage { get; set; } + + + /// + /// ProductFieldPrice. + /// + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } + + + /// + /// ProductManufacturerTag. + /// + public IEnumerable ProductManufacturerTag { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Grinder/Grinder.generated.cs b/examples/DancingGoat/Models/Reusable/ProductVariantAlphaSize/ProductVariantAlphaSize.generated.cs similarity index 53% rename from examples/DancingGoat/Models/Reusable/Grinder/Grinder.generated.cs rename to examples/DancingGoat/Models/Reusable/ProductVariantAlphaSize/ProductVariantAlphaSize.generated.cs index 32cc100..247da3f 100644 --- a/examples/DancingGoat/Models/Reusable/Grinder/Grinder.generated.cs +++ b/examples/DancingGoat/Models/Reusable/ProductVariantAlphaSize/ProductVariantAlphaSize.generated.cs @@ -16,15 +16,15 @@ namespace DancingGoat.Models { /// - /// Represents a content item of type . + /// Represents a content item of type . /// [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] - public partial class Grinder : IContentItemFieldsSource, IProductFields + public partial class ProductVariantAlphaSize : IContentItemFieldsSource, IProductSKU, IProductOptionAlphaSizes { /// /// Code name of the content type. /// - public const string CONTENT_TYPE_NAME = "DancingGoat.Grinder"; + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductVariantAlphaSize"; /// @@ -35,38 +35,14 @@ public partial class Grinder : IContentItemFieldsSource, IProductFields /// - /// GrinderManufacturer. + /// ProductSKUCode. /// - public IEnumerable GrinderManufacturer { get; set; } + public string ProductSKUCode { get; set; } /// - /// GrinderType. + /// ProductOptionAlphaSize. /// - public IEnumerable GrinderType { get; set; } - - - /// - /// ProductFieldsName. - /// - public string ProductFieldsName { get; set; } - - - /// - /// ProductFieldsDescription. - /// - public string ProductFieldsDescription { get; set; } - - - /// - /// ProductFieldsShortDescription. - /// - public string ProductFieldsShortDescription { get; set; } - - - /// - /// ProductFieldsImage. - /// - public IEnumerable ProductFieldsImage { get; set; } + public string ProductOptionAlphaSize { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs b/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs deleted file mode 100644 index 08d8b07..0000000 --- a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of links to social networks. - /// - public class SocialLinkRepository : ContentRepositoryBase - { - private readonly ILinkedItemsDependencyRetriever linkedItemsDependencyRetriever; - - - public SocialLinkRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache, ILinkedItemsDependencyRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; - } - - - /// - /// Returns list of content items. - /// - public async Task> GetSocialLinks(string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(SocialLinkRepository), nameof(GetSocialLinks), languageName); - - return await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(SocialLink.CONTENT_TYPE_NAME, config => config.WithLinkedItems(1)) - .InLanguage(languageName); - } - - - private Task> GetDependencyCacheKeys(IEnumerable socialLinks, CancellationToken cancellationToken) - { - var dependencyCacheKeys = GetCacheByIdKeys(socialLinks.Select(socialLink => socialLink.SystemFields.ContentItemID)) - .Concat(linkedItemsDependencyRetriever.Get(socialLinks.Select(link => link.SystemFields.ContentItemID), 1)) - .Append(CacheHelper.GetCacheItemName(null, ContentLanguageInfo.OBJECT_TYPE, "all")) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - - return Task.FromResult>(dependencyCacheKeys); - } - - - private static IEnumerable GetCacheByIdKeys(IEnumerable itemIds) - { - foreach (var id in itemIds) - { - yield return CacheHelper.BuildCacheItemName(new[] { "contentitem", "byid", id.ToString() }, false); - } - } - } -} diff --git a/examples/DancingGoat/Models/Reusable/Tag/TagViewModel.cs b/examples/DancingGoat/Models/Reusable/Tag/TagViewModel.cs index a7dcaaa..9d0e550 100644 --- a/examples/DancingGoat/Models/Reusable/Tag/TagViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Tag/TagViewModel.cs @@ -4,8 +4,6 @@ using CMS.ContentEngine; -using Tag = CMS.ContentEngine.Tag; - namespace DancingGoat.Models { public record TagViewModel(string Name, int Level, Guid Value, bool IsChecked = false) diff --git a/examples/DancingGoat/Models/Schema/IProductFields.generated.cs b/examples/DancingGoat/Models/Schema/IProductFields.generated.cs index d5dda02..850d93d 100644 --- a/examples/DancingGoat/Models/Schema/IProductFields.generated.cs +++ b/examples/DancingGoat/Models/Schema/IProductFields.generated.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; +using CMS.ContentEngine; namespace DancingGoat.Models { @@ -26,26 +27,38 @@ public interface IProductFields /// - /// ProductFieldsName. + /// ProductFieldName. /// - public string ProductFieldsName { get; set; } + public string ProductFieldName { get; set; } /// - /// ProductFieldsDescription. + /// ProductFieldDescription. /// - public string ProductFieldsDescription { get; set; } + public string ProductFieldDescription { get; set; } /// - /// ProductFieldsShortDescription. + /// ProductFieldImage. /// - public string ProductFieldsShortDescription { get; set; } + public IEnumerable ProductFieldImage { get; set; } /// - /// ProductFieldsImage. + /// ProductFieldPrice. /// - public IEnumerable ProductFieldsImage { get; set; } + public decimal ProductFieldPrice { get; set; } + + + /// + /// ProductFieldTags. + /// + public IEnumerable ProductFieldTags { get; set; } + + + /// + /// ProductFieldCategory. + /// + public IEnumerable ProductFieldCategory { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/Schema/IProductManufacturer.generated.cs b/examples/DancingGoat/Models/Schema/IProductManufacturer.generated.cs new file mode 100644 index 0000000..5b85897 --- /dev/null +++ b/examples/DancingGoat/Models/Schema/IProductManufacturer.generated.cs @@ -0,0 +1,34 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; + +namespace DancingGoat.Models +{ + /// + /// Defines a contract for content types with the reusable schema assigned. + /// + public interface IProductManufacturer + { + /// + /// Code name of the reusable field schema. + /// + public const string REUSABLE_FIELD_SCHEMA_NAME = "ProductManufacturer"; + + + /// + /// ProductManufacturerTag. + /// + public IEnumerable ProductManufacturerTag { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Schema/IProductOptionAlphaSizes.generated.cs b/examples/DancingGoat/Models/Schema/IProductOptionAlphaSizes.generated.cs new file mode 100644 index 0000000..b1f6f53 --- /dev/null +++ b/examples/DancingGoat/Models/Schema/IProductOptionAlphaSizes.generated.cs @@ -0,0 +1,33 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace DancingGoat.Models +{ + /// + /// Defines a contract for content types with the reusable schema assigned. + /// + public interface IProductOptionAlphaSizes + { + /// + /// Code name of the reusable field schema. + /// + public const string REUSABLE_FIELD_SCHEMA_NAME = "ProductOptionAlphaSizes"; + + + /// + /// ProductOptionAlphaSize. + /// + public string ProductOptionAlphaSize { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Schema/IProductSKU.generated.cs b/examples/DancingGoat/Models/Schema/IProductSKU.generated.cs new file mode 100644 index 0000000..385ae1c --- /dev/null +++ b/examples/DancingGoat/Models/Schema/IProductSKU.generated.cs @@ -0,0 +1,33 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace DancingGoat.Models +{ + /// + /// Defines a contract for content types with the reusable schema assigned. + /// + public interface IProductSKU + { + /// + /// Code name of the reusable field schema. + /// + public const string REUSABLE_FIELD_SCHEMA_NAME = "ProductSKU"; + + + /// + /// ProductSKUCode. + /// + public string ProductSKUCode { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs index 63a2117..d66e59a 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using CMS.Websites; namespace DancingGoat.Models { - public record ArticleDetailViewModel(string Title, string TeaserUrl, string Summary, string Text, DateTime PublicationDate, Guid Guid, bool IsSecured, string Url, IEnumerable RelatedArticles) + public record ArticleDetailViewModel(string Title, string TeaserUrl, string Summary, string Text, DateTime PublicationDate, Guid Guid, bool IsSecured, string Url, IEnumerable RelatedPages) : IWebPageBasedViewModel { /// @@ -17,22 +16,17 @@ public record ArticleDetailViewModel(string Title, string TeaserUrl, string Summ /// /// Validates and maps to a . /// - public static async Task GetViewModel(ArticlePage articlePage, string languageName, ArticlePageRepository articlePageRepository, IWebPageUrlRetriever urlRetriever) + public static ArticleDetailViewModel GetViewModel(ArticlePage articlePage) { var teaser = articlePage.ArticlePageTeaser.FirstOrDefault(); - var relatedArticles = await articlePageRepository - .GetArticles(articlePage.ArticleRelatedArticles.Select(article => article.WebPageGuid).ToList(), languageName); + var relatedPageViewModels = new List(); - var relatedArticlesViewModels = new List(); - - foreach (var relatedArticle in relatedArticles) + foreach (var relatedPage in articlePage.ArticleRelatedPages) { - relatedArticlesViewModels.Add(await RelatedArticleViewModel.GetViewModel(relatedArticle, urlRetriever, languageName)); + relatedPageViewModels.Add(RelatedPageViewModel.GetViewModel(relatedPage)); } - var url = await urlRetriever.Retrieve(articlePage, languageName); - return new ArticleDetailViewModel( articlePage.ArticleTitle, teaser?.ImageFile.Url, @@ -41,8 +35,8 @@ public static async Task GetViewModel(ArticlePage articl articlePage.ArticlePagePublishDate, articlePage.SystemFields.ContentItemGUID, articlePage.SystemFields.ContentItemIsSecured, - url.RelativePath, - relatedArticlesViewModels) + articlePage.GetUrl().RelativePath, + relatedPageViewModels) { WebPage = articlePage }; diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePage.generated.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePage.generated.cs index e3b46a9..2a76a61 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePage.generated.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePage.generated.cs @@ -66,9 +66,9 @@ public partial class ArticlePage : IWebPageFieldsSource, ISEOFields /// - /// ArticleRelatedArticles. + /// ArticleRelatedPages. /// - public IEnumerable ArticleRelatedArticles { get; set; } + public IEnumerable ArticleRelatedPages { get; set; } /// diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs deleted file mode 100644 index c9b6325..0000000 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.DataEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of article pages. - /// - public class ArticlePageRepository : ContentRepositoryBase - { - private readonly IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever; - - - /// - /// Initializes new instance of . - /// - public ArticlePageRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IProgressiveCache cache, - IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; - } - - - /// - /// Returns list of web pages. - /// - public async Task> GetArticles(string treePath, string languageName, bool includeSecuredItems, int topN = 0, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(topN, treePath, languageName); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = includeSecuredItems - }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, treePath, languageName, includeSecuredItems, topN); - - return await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - /// - /// Returns list of content items with guids passed in parameter. - /// - public async Task> GetArticles(ICollection guids, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(guids, languageName); - - var options = new ContentQueryExecutionOptions { IncludeSecuredItems = true }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, languageName, guids.GetHashCode()); - - return await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - /// - /// Returns web page by ID and language name. - /// - public async Task GetArticle(int id, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(id, languageName); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = true - }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ArticlePage), id, languageName); - - var result = await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - - private ContentItemQueryBuilder GetQueryBuilder(int topN, string treePath, string languageName) - { - return GetQueryBuilder( - languageName, - config => config - .WithLinkedItems(1) - .TopN(topN) - .OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate))) - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Children(treePath))); - } - - - private ContentItemQueryBuilder GetQueryBuilder(ICollection guids, string languageName) - { - return new ContentItemQueryBuilder().ForContentTypes(q => - { - q.ForWebsite(guids) - .WithContentTypeFields() - .WithLinkedItems(1); - }).InLanguage(languageName) - .Parameters(q => - q.OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate)))); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) - { - return GetQueryBuilder( - languageName, - config => config - .WithLinkedItems(1) - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), id))); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(string languageName, Action configureQuery = null) - { - return new ContentItemQueryBuilder() - .ForContentType(ArticlePage.CONTENT_TYPE_NAME, configureQuery) - .InLanguage(languageName); - } - - - private async Task> GetDependencyCacheKeys(IEnumerable articles, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - foreach (var article in articles) - { - dependencyCacheKeys.UnionWith(GetDependencyCacheKeys(article)); - } - - dependencyCacheKeys.UnionWith(await webPageLinkedItemsDependencyRetriever.Get(articles.Select(articlePage => articlePage.SystemFields.WebPageItemID), 1, cancellationToken)); - dependencyCacheKeys.Add(CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID)); - - return dependencyCacheKeys; - } - - - private IEnumerable GetDependencyCacheKeys(ArticlePage article) - { - if (article == null) - { - return Enumerable.Empty(); - } - - return new List() - { - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", article.SystemFields.WebPageItemID.ToString() }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "bypath", article.SystemFields.WebPageItemTreePath }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "childrenofpath", DataHelper.GetParentPath(article.SystemFields.WebPageItemTreePath) }, false), - CacheHelper.GetCacheItemName(null, ContentLanguageInfo.OBJECT_TYPE, "all") - }; - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs index 9410c3d..a2ed46e 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using CMS.Websites; @@ -12,11 +10,11 @@ public record ArticleViewModel(string Title, string TeaserUrl, string Summary, s /// /// Validates and maps to a . /// - public static async Task GetViewModel(ArticlePage articlePage, IWebPageUrlRetriever urlRetriever, string languageName, CancellationToken cancellationToken = default) + public static ArticleViewModel GetViewModel(ArticlePage articlePage) { var teaser = articlePage.ArticlePageTeaser.FirstOrDefault(); - var url = await urlRetriever.Retrieve(articlePage, languageName, cancellationToken); + var url = articlePage.GetUrl(); return new ArticleViewModel( articlePage.ArticleTitle, diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs deleted file mode 100644 index e65daf2..0000000 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -using CMS.Websites; - -namespace DancingGoat.Models -{ - public record RelatedArticleViewModel(string Title, string TeaserUrl, string Summary, string Text, DateTime PublicationDate, Guid Guid, string Url) - { - /// - /// Validates and maps to a . - /// - public static async Task GetViewModel(ArticlePage articlePage, IWebPageUrlRetriever urlRetriever, string languageName) - { - var url = await urlRetriever.Retrieve(articlePage, languageName); - - return new RelatedArticleViewModel - ( - articlePage.ArticleTitle, - articlePage.ArticlePageTeaser.FirstOrDefault()?.ImageFile.Url, - articlePage.ArticlePageSummary, - articlePage.ArticlePageText, - articlePage.ArticlePagePublishDate, - articlePage.SystemFields.ContentItemGUID, - url.RelativePath - ); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedPageViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedPageViewModel.cs new file mode 100644 index 0000000..b8fa69d --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedPageViewModel.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Net; + +using CMS.Websites; + +namespace DancingGoat.Models +{ + public record RelatedPageViewModel(string Title, string TeaserUrl, string Summary, DateTime? PublicationDate, string Url) + { + /// + /// Validates and maps or to a . + /// + public static RelatedPageViewModel GetViewModel(IWebPageFieldsSource webPage) + { + if (webPage is ArticlePage article) + { + return GetViewModelFromArticlePage(article); + } + else if (webPage is ProductPage productPage) + { + return GetViewModelFromProductPage(productPage); + } + + throw new ArgumentException($"Param {nameof(webPage)} must be {nameof(ArticlePage)} or {nameof(ProductPage)}"); + } + + + private static RelatedPageViewModel GetViewModelFromArticlePage(ArticlePage articlePage) + { + return new RelatedPageViewModel + ( + articlePage.ArticleTitle, + articlePage.ArticlePageTeaser.FirstOrDefault()?.ImageFile.Url, + WebUtility.HtmlEncode(articlePage.ArticlePageSummary), + articlePage.ArticlePagePublishDate, + articlePage.GetUrl().RelativePath + ); + } + + + private static RelatedPageViewModel GetViewModelFromProductPage(ProductPage productPage) + { + var product = productPage.ProductPageProduct.FirstOrDefault() as IProductFields; + + return new RelatedPageViewModel + ( + product?.ProductFieldName, + product?.ProductFieldImage.FirstOrDefault()?.ImageFile.Url ?? string.Empty, + product?.ProductFieldDescription, + null, + productPage.GetUrl().RelativePath + ); + } + } +} diff --git a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs b/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs deleted file mode 100644 index 390f96d..0000000 --- a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - public class ArticlesSectionRepository : ContentRepositoryBase - { - /// - /// Initializes new instance of . - /// - public ArticlesSectionRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns web page by ID and language name. - /// - public async Task GetArticlesSection(int id, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(id, languageName); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = true - }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ArticlesSection), id, languageName); - - var result = await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - /// - /// Returns web page by GUID and language name. - /// - public async Task GetArticlesSection(Guid guid, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(guid, languageName); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = true - }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ArticlesSection), guid, languageName); - - var result = await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private Task> GetDependencyCacheKeys(IEnumerable articlesSections, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - foreach (var articlesSection in articlesSections) - { - dependencyCacheKeys.UnionWith(GetDependencyCacheKeys(articlesSection)); - } - - dependencyCacheKeys.Add(CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID)); - - return Task.FromResult>(dependencyCacheKeys); - } - - - private static IEnumerable GetDependencyCacheKeys(ArticlesSection articleSection) - { - if (articleSection == null) - { - return Enumerable.Empty(); - } - - var cacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", articleSection.SystemFields.WebPageItemID.ToString() }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byguid", articleSection.SystemFields.WebPageItemGUID.ToString() }, false), - }; - - return cacheKeys; - } - - - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(ArticlesSection.CONTENT_TYPE_NAME, - config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(WebPageFields.WebPageItemID), id)) - .TopN(1)) - .InLanguage(languageName); - } - - - private ContentItemQueryBuilder GetQueryBuilder(Guid guid, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(ArticlesSection.CONTENT_TYPE_NAME, - config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemGUID), guid)) - .TopN(1)) - .InLanguage(languageName); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.generated.cs b/examples/DancingGoat/Models/WebPage/Checkout/Checkout.generated.cs similarity index 67% rename from examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.generated.cs rename to examples/DancingGoat/Models/WebPage/Checkout/Checkout.generated.cs index 5045100..ef518db 100644 --- a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.generated.cs +++ b/examples/DancingGoat/Models/WebPage/Checkout/Checkout.generated.cs @@ -1,4 +1,4 @@ -//-------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------- // // // This code was generated by code generator tool. @@ -17,15 +17,15 @@ namespace DancingGoat.Models { /// - /// Represents a page of type . + /// Represents a page of type . /// [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] - public partial class CoffeePage : IWebPageFieldsSource + public partial class Checkout : IWebPageFieldsSource { /// /// Code name of the content type. /// - public const string CONTENT_TYPE_NAME = "DancingGoat.CoffeePage"; + public const string CONTENT_TYPE_NAME = "DancingGoat.Checkout"; /// @@ -33,11 +33,5 @@ public partial class CoffeePage : IWebPageFieldsSource /// [SystemField] public WebPageFields SystemFields { get; set; } - - - /// - /// RelatedItem. - /// - public IEnumerable RelatedItem { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutFormConstants.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutFormConstants.cs new file mode 100644 index 0000000..5106d8c --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutFormConstants.cs @@ -0,0 +1,18 @@ +namespace DancingGoat.Models +{ + /// + /// Constants for the checkout form. + /// + public static class CheckoutFormConstants + { + /// + /// The error message displayed when a required field is not filled in. + /// + public const string REQUIRED_FIELD_ERROR_MESSAGE = "The field is required."; + + /// + /// The error message displayed when the input exceeds the maximum allowed length. + /// + public const string MAX_LENGTH_ERROR_MESSAGE = "The input exceeds the maximum allowed length."; + } +} diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutStep.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutStep.cs new file mode 100644 index 0000000..a27e5cb --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutStep.cs @@ -0,0 +1,17 @@ +namespace DancingGoat.Models; + +/// +/// Represents the steps in the checkout process. +/// +public enum CheckoutStep +{ + /// + /// Represents the step for entering customer shipping information. + /// + CheckoutCustomer = 1, + + /// + /// Represents the step for confirming the order. + /// + OrderConfirmation = 2 +} diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutViewModel.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutViewModel.cs new file mode 100644 index 0000000..3284993 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CheckoutViewModel.cs @@ -0,0 +1,3 @@ +namespace DancingGoat.Models; + +public sealed record CheckoutViewModel(CheckoutStep Step, CustomerViewModel Customer, CustomerAddressViewModel CustomerAddress, ShoppingCartViewModel ShoppingCart); diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/ConfirmOrderViewModel.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/ConfirmOrderViewModel.cs new file mode 100644 index 0000000..cca13e5 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/ConfirmOrderViewModel.cs @@ -0,0 +1,3 @@ +namespace DancingGoat.Models; + +public sealed record ConfirmOrderViewModel(string OrderNumber); diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerAddressViewModel.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerAddressViewModel.cs new file mode 100644 index 0000000..7bbe6a0 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerAddressViewModel.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +using Microsoft.AspNetCore.Mvc.Rendering; + +using static DancingGoat.Models.CheckoutFormConstants; + +namespace DancingGoat.Models; + +public sealed record CustomerAddressViewModel +{ + public CustomerAddressViewModel() + { + Countries = new List(); + States = new List(); + } + + + public CustomerAddressViewModel(IEnumerable countries) + { + Countries = countries; + States = new List(); + } + + + [Display(Name = "Street address")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(200, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string Line1 { get; set; } + + [Display(Name = "Apartment, suite, unit, etc.")] + [MaxLength(200, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string Line2 { get; set; } + + [Display(Name = "City")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(100, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string City { get; set; } + + [Display(Name = "Postal code")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(10, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string PostalCode { get; set; } + + [Display(Name = "Country")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + public string CountryId { get; set; } + + [Display(Name = "State")] + public string StateId { get; set; } + + public string Country { get; set; } + + public string State { get; set; } + + public IEnumerable Countries { get; set; } + + public IEnumerable States { get; set; } +} diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerDto.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerDto.cs new file mode 100644 index 0000000..d23cba5 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerDto.cs @@ -0,0 +1,29 @@ +namespace DancingGoat.Models; + +/// +/// Data transfer object for customer information in the checkout process. +/// +public sealed record CustomerDto +{ + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Email { get; set; } + + public string PhoneNumber { get; set; } + + public string Company { get; set; } + + public string AddressLine1 { get; set; } + + public string AddressLine2 { get; set; } + + public string AddressCity { get; set; } + + public string AddressPostalCode { get; set; } + + public int AddressCountryId { get; set; } + + public int AddressStateId { get; set; } +} diff --git a/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerViewModel.cs b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerViewModel.cs new file mode 100644 index 0000000..601268a --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/CheckoutPage/CustomerViewModel.cs @@ -0,0 +1,77 @@ +using System.ComponentModel.DataAnnotations; + +using static DancingGoat.Models.CheckoutFormConstants; + +namespace DancingGoat.Models; + +public sealed record CustomerViewModel +{ + public CustomerViewModel() + { + FirstName = LastName = Email = PhoneNumber = string.Empty; + } + + + public CustomerViewModel(string firstName, string lastName, string email, string phoneNumber) + { + FirstName = firstName; + LastName = lastName; + Email = email; + PhoneNumber = phoneNumber; + } + + + [Display(Name = "First name")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(100, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string FirstName { get; set; } + + [Display(Name = "Last name")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(200, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string LastName { get; set; } + + [Display(Name = "Company")] + [MaxLength(200, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string Company { get; set; } + + [Display(Name = "Email")] + [EmailAddress(ErrorMessage = "Please enter a valid email address.")] + [Required(ErrorMessage = REQUIRED_FIELD_ERROR_MESSAGE)] + [MaxLength(255, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string Email { get; set; } + + [Display(Name = "Phone")] + [Phone(ErrorMessage = "Please enter a valid phone number.")] + [MaxLength(30, ErrorMessage = MAX_LENGTH_ERROR_MESSAGE)] + public string PhoneNumber { get; set; } + + + public bool IsEmpty() + { + return string.IsNullOrEmpty(FirstName) && string.IsNullOrEmpty(LastName) && string.IsNullOrEmpty(Company) && string.IsNullOrEmpty(Email) && string.IsNullOrEmpty(PhoneNumber); + } + + + public CustomerDto ToCustomerDto(CustomerAddressViewModel customerAddressViewModel) + { + int.TryParse(customerAddressViewModel.CountryId, out int countryId); + int.TryParse(customerAddressViewModel.StateId, out int stateId); + + return new CustomerDto + { + FirstName = FirstName, + LastName = LastName, + Email = Email, + PhoneNumber = PhoneNumber, + + Company = Company, + AddressLine1 = customerAddressViewModel.Line1, + AddressLine2 = customerAddressViewModel.Line2, + AddressCity = customerAddressViewModel.City, + AddressPostalCode = customerAddressViewModel.PostalCode, + AddressCountryId = countryId, + AddressStateId = stateId + }; + } +} diff --git a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeeDetailViewModel.cs b/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeeDetailViewModel.cs deleted file mode 100644 index ae380f3..0000000 --- a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeeDetailViewModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using CMS.ContentEngine; - -using Tag = CMS.ContentEngine.Tag; - -namespace DancingGoat.Models -{ - public record CoffeeDetailViewModel(string Name, string Description, string ImageUrl, IEnumerable Tastes, IEnumerable Processing) - { - /// - /// Maps to a . - /// - public async static Task GetViewModel(CoffeePage coffeePage, string languageName, ITaxonomyRetriever taxonomyRetriever) - { - var coffee = coffeePage.RelatedItem.FirstOrDefault(); - var image = coffee.ProductFieldsImage.FirstOrDefault(); - - return new CoffeeDetailViewModel( - coffee.ProductFieldsName, - coffee.ProductFieldsDescription, - image?.ImageFile.Url, - await taxonomyRetriever.RetrieveTags(coffee.CoffeeTastes.Select(taste => taste.Identifier), languageName), - await taxonomyRetriever.RetrieveTags(coffee.CoffeeProcessing.Select(processing => processing.Identifier), languageName) - ); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.cs b/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.cs deleted file mode 100644 index 57542c5..0000000 --- a/examples/DancingGoat/Models/WebPage/CoffeePage/CoffeePage.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace DancingGoat.Models -{ - /// - /// Custom code for page of type . - /// - public partial class CoffeePage : IProductPage - { - /// - IEnumerable IProductPage.RelatedItem { get => RelatedItem; } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPage.generated.cs b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPage.generated.cs index 5c0fcd1..8d0658d 100644 --- a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPage.generated.cs +++ b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPage.generated.cs @@ -20,7 +20,7 @@ namespace DancingGoat.Models /// Represents a page of type . /// [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] - public partial class ConfirmationPage : IWebPageFieldsSource, ISEOFields + public partial class ConfirmationPage : IWebPageFieldsSource { /// /// Code name of the content type. @@ -57,23 +57,5 @@ public partial class ConfirmationPage : IWebPageFieldsSource, ISEOFields /// ConfirmationPageArticlesSection. /// public IEnumerable ConfirmationPageArticlesSection { get; set; } - - - /// - /// SEOFieldsTitle. - /// - public string SEOFieldsTitle { get; set; } - - - /// - /// SEOFieldsDescription. - /// - public string SEOFieldsDescription { get; set; } - - - /// - /// SEOFieldsAllowSearchIndexing. - /// - public bool SEOFieldsAllowSearchIndexing { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs deleted file mode 100644 index 6a9179c..0000000 --- a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of confirmation pages. - /// - public class ConfirmationPageRepository : ContentRepositoryBase - { - public ConfirmationPageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns content item. - /// - /// Web page item ID. - /// Language name. - /// Cancellation token. - public async Task GetConfirmationPage(int webPageItemId, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(webPageItemId, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ConfirmationPage), webPageItemId, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(ConfirmationPage.CONTENT_TYPE_NAME, config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, includeUrlPath: false) - .Where(where => where - .WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) - .TopN(1)) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable confirmationPages, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var confirmationPage = confirmationPages.FirstOrDefault(); - - if (confirmationPage != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", confirmationPage.SystemFields.WebPageItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsPageRepository.cs b/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsPageRepository.cs deleted file mode 100644 index 4133065..0000000 --- a/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsPageRepository.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - public class ContactsPageRepository : ContentRepositoryBase - { - /// - /// Initializes new instance of . - /// - public ContactsPageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns web page by tree path and language name. - /// - public async Task GetContactsPage(string treePath, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(w => w.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemTreePath), treePath), languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ContactsPage), nameof(IWebPageContentQueryDataContainer.WebPageItemTreePath), treePath, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - /// - /// Returns web page by ID and language name. - /// - public async Task GetContactsPage(int webPageItemId, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(w => w.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId), languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ContactsPage), nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(Action where, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(ContactsPage.CONTENT_TYPE_NAME, config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where) - .TopN(1)) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable contactsPages, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var contactsPage = contactsPages.FirstOrDefault(); - if (contactsPage != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", contactsPage.SystemFields.WebPageItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderDetailViewModel.cs b/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderDetailViewModel.cs deleted file mode 100644 index 5079460..0000000 --- a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderDetailViewModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using CMS.ContentEngine; - -using Tag = CMS.ContentEngine.Tag; - -namespace DancingGoat.Models -{ - public record GrinderDetailViewModel(string Name, string Description, string ImageUrl, IEnumerable Manufacturers, IEnumerable Type) - { - /// - /// Maps to a . - /// - public async static Task GetViewModel(GrinderPage grinderPage, string languageName, ITaxonomyRetriever taxonomyRetriever) - { - var grinder = grinderPage.RelatedItem.FirstOrDefault(); - var image = grinder.ProductFieldsImage.FirstOrDefault(); - - return new GrinderDetailViewModel( - grinder.ProductFieldsName, - grinder.ProductFieldsDescription, - image?.ImageFile.Url, - await taxonomyRetriever.RetrieveTags(grinder.GrinderManufacturer.Select(manufacturer => manufacturer.Identifier), languageName), - await taxonomyRetriever.RetrieveTags(grinder.GrinderType.Select(type => type.Identifier), languageName) - ); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.cs b/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.cs deleted file mode 100644 index ded6ed2..0000000 --- a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace DancingGoat.Models -{ - /// - /// Custom code for page of type . - /// - public partial class GrinderPage : IProductPage - { - /// - IEnumerable IProductPage.RelatedItem { get => RelatedItem; } - } -} diff --git a/examples/DancingGoat/Models/WebPage/HomePage/HomePage.generated.cs b/examples/DancingGoat/Models/WebPage/HomePage/HomePage.generated.cs index 4585e10..65f5f3c 100644 --- a/examples/DancingGoat/Models/WebPage/HomePage/HomePage.generated.cs +++ b/examples/DancingGoat/Models/WebPage/HomePage/HomePage.generated.cs @@ -60,9 +60,9 @@ public partial class HomePage : IWebPageFieldsSource, ISEOFields /// - /// HomePageCafes. + /// HomePageCafesFolder. /// - public IEnumerable HomePageCafes { get; set; } + public SmartFolderReference HomePageCafesFolder { get; set; } /// diff --git a/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs b/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs deleted file mode 100644 index c9614e3..0000000 --- a/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of home pages. - /// - public class HomePageRepository : ContentRepositoryBase - { - private readonly IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever; - - - /// - /// Initializes new instance of . - /// - public HomePageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache, IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; - } - - - /// - /// Returns content item. - /// - public async Task GetHomePage(int webPageItemId, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(webPageItemId, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(HomePage), languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(HomePage.CONTENT_TYPE_NAME, - config => config - .WithLinkedItems(4) - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) - .TopN(1)) - .InLanguage(languageName); - } - - - private async Task> GetDependencyCacheKeys(IEnumerable homePages, CancellationToken cancellationToken) - { - var homePage = homePages.FirstOrDefault(); - - if (homePage == null) - { - return new HashSet(); - } - - return (await webPageLinkedItemsDependencyRetriever.Get(homePage.SystemFields.WebPageItemID, 4, cancellationToken)) - .Concat(GetCacheByGuidKeys(homePage.HomePageArticlesSection.Select(articlesSection => articlesSection.WebPageGuid))) - .Append(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", homePage.SystemFields.WebPageItemID.ToString() }, false)) - .Append(CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID)) - .Append(CacheHelper.GetCacheItemName(null, ContentLanguageInfo.OBJECT_TYPE, "all")) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - } - - - private static IEnumerable GetCacheByGuidKeys(IEnumerable webPageGuids) - { - foreach (var guid in webPageGuids) - { - yield return CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byguid", guid.ToString() }, false); - } - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs b/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs index 5fde574..6d3063c 100644 --- a/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs @@ -16,7 +16,7 @@ public record HomePageViewModel(BannerViewModel Banner, EventViewModel Event, st /// /// Validates and maps to a . /// - public static HomePageViewModel GetViewModel(HomePage home) + public static HomePageViewModel GetViewModel(HomePage home, IEnumerable cafes) { if (home == null) { @@ -28,7 +28,7 @@ public static HomePageViewModel GetViewModel(HomePage home) EventViewModel.GetViewModel(home.HomePageEvent.OrderBy(o => Math.Abs((o.EventDate - DateTime.Today).TotalDays)).FirstOrDefault()), home.HomePageOurStory, ReferenceViewModel.GetViewModel(home.HomePageReference.FirstOrDefault()), - home.HomePageCafes.Select(CafeViewModel.GetViewModel), + cafes.Select(CafeViewModel.GetViewModel), home.HomePageArticlesSection.FirstOrDefault()) { WebPage = home diff --git a/examples/DancingGoat/Models/WebPage/LandingPage/LandingPageRepository.cs b/examples/DancingGoat/Models/WebPage/LandingPage/LandingPageRepository.cs deleted file mode 100644 index 7b4c717..0000000 --- a/examples/DancingGoat/Models/WebPage/LandingPage/LandingPageRepository.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of landing pages. - /// - public class LandingPageRepository : ContentRepositoryBase - { - public LandingPageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - /// - /// Returns content item. - /// - /// Web page item ID. - /// Language name. - /// Cancellation token. - public async Task GetLandingPage(int webPageItemId, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(webPageItemId, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(LandingPage), webPageItemId, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(LandingPage.CONTENT_TYPE_NAME, config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, includeUrlPath: false) - .Where(where => where - .WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) - .TopN(1)) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable confirmationPages, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var confirmationPage = confirmationPages.FirstOrDefault(); - - if (confirmationPage != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", confirmationPage.SystemFields.WebPageItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs b/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs deleted file mode 100644 index 2d6a49e..0000000 --- a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of navigation items. - /// - public class NavigationItemRepository : ContentRepositoryBase - { - /// - /// Initializes new instance of . - /// - public NavigationItemRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns list of content items representing navigation menu. - /// - public async Task> GetNavigationItems(string languageName, CancellationToken cancellationToken) - { - var query = new ContentItemQueryBuilder() - .ForContentType(NavigationItem.CONTENT_TYPE_NAME, config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Children(DancingGoatConstants.NAVIGATION_MENU_FOLDER_PATH), includeUrlPath: false) - .OrderBy(nameof(IWebPageContentQueryDataContainer.WebPageItemOrder))) - .InLanguage(languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(GetNavigationItems), languageName); - - return await GetCachedQueryResult(query, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - private Task> GetDependencyCacheKeys(IEnumerable navigationItems, CancellationToken cancellationToken) - { - if (navigationItems == null) - { - return Task.FromResult>(new HashSet()); - } - - var dependencyCacheKeys = GetCacheKeys(navigationItems.Select(navItem => navItem.SystemFields.WebPageItemID)) - .Append(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "childrenofpath", DancingGoatConstants.NAVIGATION_MENU_FOLDER_PATH })) - .ToHashSet(StringComparer.InvariantCultureIgnoreCase); - - return Task.FromResult>(dependencyCacheKeys); - } - - - private static IEnumerable GetCacheKeys(IEnumerable itemIds) - { - foreach (int id in itemIds) - { - yield return CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", id.ToString() }, false); - } - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/PrivacyPage/PrivacyPageRepository.cs b/examples/DancingGoat/Models/WebPage/PrivacyPage/PrivacyPageRepository.cs deleted file mode 100644 index dfcc233..0000000 --- a/examples/DancingGoat/Models/WebPage/PrivacyPage/PrivacyPageRepository.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - public class PrivacyPageRepository : ContentRepositoryBase - { - /// - /// Initializes new instance of . - /// - public PrivacyPageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache) - : base(websiteChannelContext, executor, cache) - { - } - - - /// - /// Returns web page by ID and language name. - /// - public async Task GetPrivacyPage(int webPageItemId, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(webPageItemId, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(PrivacyPage), webPageItemId, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(PrivacyPage.CONTENT_TYPE_NAME, config => config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, includeUrlPath: false) - .Where(where => where - .WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) - .TopN(1)) - .InLanguage(languageName); - } - - - private static Task> GetDependencyCacheKeys(IEnumerable privacyPages, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(); - - var privacyPage = privacyPages.FirstOrDefault(); - if (privacyPage != null) - { - dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", privacyPage.SystemFields.WebPageItemID.ToString() }, false)); - } - - return Task.FromResult>(dependencyCacheKeys); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ProductCategory/ProductCategory.generated.cs b/examples/DancingGoat/Models/WebPage/ProductCategory/ProductCategory.generated.cs new file mode 100644 index 0000000..c03b6d8 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ProductCategory/ProductCategory.generated.cs @@ -0,0 +1,67 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; +using CMS.Websites; + +namespace DancingGoat.Models +{ + /// + /// Represents a page of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ProductCategory : IWebPageFieldsSource, ISEOFields + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductCategory"; + + + /// + /// Represents system properties for a web page item. + /// + [SystemField] + public WebPageFields SystemFields { get; set; } + + + /// + /// ProductType. + /// + public string ProductType { get; set; } + + + /// + /// ProductCategoryTag. + /// + public IEnumerable ProductCategoryTag { get; set; } + + + /// + /// SEOFieldsTitle. + /// + public string SEOFieldsTitle { get; set; } + + + /// + /// SEOFieldsDescription. + /// + public string SEOFieldsDescription { get; set; } + + + /// + /// SEOFieldsAllowSearchIndexing. + /// + public bool SEOFieldsAllowSearchIndexing { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ProductCategory/ProductListingViewModel.cs b/examples/DancingGoat/Models/WebPage/ProductCategory/ProductListingViewModel.cs new file mode 100644 index 0000000..ee03bc9 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ProductCategory/ProductListingViewModel.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; + +using CMS.ContentEngine; +using CMS.Websites; + +namespace DancingGoat.Models +{ + public record ProductListingViewModel(ProductSectionListViewModel SelectionProductListViewModel, IEnumerable CategoryMenuViewModel) : IWebPageBasedViewModel + { + /// + public IWebPageFieldsSource WebPage { get; init; } + + + /// + /// Validates and maps to a . + /// + public static ProductListingViewModel GetViewModel(ProductCategory productCategory, IEnumerable products, IDictionary productPageUrls, TaxonomyData productTagsTaxonomy, + IEnumerable categoryMenu, string languageName) + { + if (productCategory == null) + { + return null; + } + + var selection = new ProductSectionListViewModel(null, + products + .Select(product => + { + productPageUrls.TryGetValue((product as IContentItemFieldsSource).SystemFields.ContentItemID, out string pageUrl); + + return ProductListItemViewModel.GetViewModel( + product, + pageUrl, + productTagsTaxonomy.Tags.FirstOrDefault(tag => tag.Identifier == product.ProductFieldTags.FirstOrDefault()?.Identifier)?.Title); + }) + .OrderBy(product => product.Name)); + + return new ProductListingViewModel(selection, categoryMenu) + { + WebPage = productCategory + }; + } + } +} diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/IProductPage.cs b/examples/DancingGoat/Models/WebPage/ProductPage/IProductPage.cs deleted file mode 100644 index 82e308f..0000000 --- a/examples/DancingGoat/Models/WebPage/ProductPage/IProductPage.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -using CMS.Websites; - -namespace DancingGoat.Models -{ - /// - /// Represents a common product page model. - /// - public interface IProductPage : IWebPageFieldsSource - { - /// - /// Get product related item. - /// - public IEnumerable RelatedItem { get; } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/ProductListItemViewModel.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductListItemViewModel.cs index 35d8c22..49e26dd 100644 --- a/examples/DancingGoat/Models/WebPage/ProductPage/ProductListItemViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ProductPage/ProductListItemViewModel.cs @@ -1,24 +1,17 @@ using System.Linq; -using System.Threading.Tasks; - -using CMS.Websites; namespace DancingGoat.Models { - public record ProductListItemViewModel(string Name, string ImagePath, string Url) + public record ProductListItemViewModel(string Name, string ImagePath, string Url, decimal Price, string Tag) { - public static async Task GetViewModel(IProductPage productPage, IWebPageUrlRetriever urlRetriever, string languageName) + public static ProductListItemViewModel GetViewModel(IProductFields product, string urlPath, string tag) { - var product = productPage.RelatedItem.FirstOrDefault(); - var image = product.ProductFieldsImage.FirstOrDefault(); - - var path = (await urlRetriever.Retrieve(productPage, languageName)).RelativePath; - return new ProductListItemViewModel( - product.ProductFieldsName, - image?.ImageFile.Url, - path - ); + product.ProductFieldName, + product.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, + urlPath, + product.ProductFieldPrice, + tag); } } } diff --git a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.generated.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductPage.generated.cs similarity index 77% rename from examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.generated.cs rename to examples/DancingGoat/Models/WebPage/ProductPage/ProductPage.generated.cs index d79dda1..867bdc4 100644 --- a/examples/DancingGoat/Models/WebPage/GrinderPage/GrinderPage.generated.cs +++ b/examples/DancingGoat/Models/WebPage/ProductPage/ProductPage.generated.cs @@ -17,15 +17,15 @@ namespace DancingGoat.Models { /// - /// Represents a page of type . + /// Represents a page of type . /// [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] - public partial class GrinderPage : IWebPageFieldsSource + public partial class ProductPage : IWebPageFieldsSource { /// /// Code name of the content type. /// - public const string CONTENT_TYPE_NAME = "DancingGoat.GrinderPage"; + public const string CONTENT_TYPE_NAME = "DancingGoat.ProductPage"; /// @@ -36,8 +36,8 @@ public partial class GrinderPage : IWebPageFieldsSource /// - /// RelatedItem. + /// ProductPageProduct. /// - public IEnumerable RelatedItem { get; set; } + public IEnumerable ProductPageProduct { get; set; } } } \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/ProductPageRepository.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductPageRepository.cs deleted file mode 100644 index f2f12e5..0000000 --- a/examples/DancingGoat/Models/WebPage/ProductPage/ProductPageRepository.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - /// - /// Represents a collection of product pages. - /// - public class ProductPageRepository : ContentRepositoryBase - { - private readonly IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever; - - - /// - /// Initializes new instance of . - /// - public ProductPageRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IProgressiveCache cache, - IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; - } - - - /// - /// Returns list of web pages. - /// - public async Task> GetProducts(string treePath, string languageName, IEnumerable linkedProducts, bool includeSecuredItems = true, CancellationToken cancellationToken = default) - { - if (!linkedProducts.Any()) - { - return Enumerable.Empty(); - } - - var queryBuilder = GetQueryBuilder(treePath, languageName, linkedProducts); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = includeSecuredItems - }; - - var linkedProductCacheParts = linkedProducts.Select(product => product.ProductFieldsName).Join("|"); - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, treePath, languageName, includeSecuredItems, nameof(IProductPage), linkedProductCacheParts); - - return await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - } - - - public async Task GetProduct(string contentTypeName, int id, string languageName, bool includeSecuredItems = true, CancellationToken cancellationToken = default) - where ProductPageType : IWebPageFieldsSource, new() - { - var queryBuilder = GetQueryBuilder(id, languageName, contentTypeName); - - var options = new ContentQueryExecutionOptions - { - IncludeSecuredItems = includeSecuredItems - }; - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ProductPageType), id, languageName); - - var result = await GetCachedQueryResult(queryBuilder, options, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private ContentItemQueryBuilder GetQueryBuilder(string treePath, string languageName, IEnumerable linkedProducts) - { - return GetQueryBuilder( - languageName, - config => config - .Linking(nameof(IProductPage.RelatedItem), linkedProducts.Select(linkedProduct => ((IContentItemFieldsSource)linkedProduct).SystemFields.ContentItemID)) - .WithLinkedItems(2) - .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Children(treePath))); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(string languageName, Action configure = null) - { - return new ContentItemQueryBuilder() - .ForContentType(CoffeePage.CONTENT_TYPE_NAME, configure) - .ForContentType(GrinderPage.CONTENT_TYPE_NAME, configure) - .InLanguage(languageName); - } - - - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName, string contentTypeName) - { - return GetQueryBuilder( - languageName, - contentTypeName, - config => config - .WithLinkedItems(2) - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), id))); - } - - - private static ContentItemQueryBuilder GetQueryBuilder(string languageName, string contentTypeName, Action configureQuery = null) - { - return new ContentItemQueryBuilder() - .ForContentType(contentTypeName, configureQuery) - .InLanguage(languageName); - } - - - private async Task> GetDependencyCacheKeys(IEnumerable products, CancellationToken cancellationToken) - where ProductPageType : IWebPageFieldsSource - { - var dependencyCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - foreach (var product in products) - { - dependencyCacheKeys.UnionWith(GetDependencyCacheKeys(product)); - } - - dependencyCacheKeys.UnionWith(await webPageLinkedItemsDependencyRetriever.Get(products.Select(productPage => productPage.SystemFields.WebPageItemID), 1, cancellationToken)); - dependencyCacheKeys.Add(CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID)); - - return dependencyCacheKeys; - } - - - private IEnumerable GetDependencyCacheKeys(IWebPageFieldsSource product) - { - if (product == null) - { - return Enumerable.Empty(); - } - - return new List() - { - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", product.SystemFields.WebPageItemID.ToString() }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "bypath", product.SystemFields.WebPageItemTreePath }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "childrenofpath", DataHelper.GetParentPath(product.SystemFields.WebPageItemTreePath) }, false), - CacheHelper.GetCacheItemName(null, ContentLanguageInfo.OBJECT_TYPE, "all") - }; - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/ProductRepository.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductRepository.cs new file mode 100644 index 0000000..6017322 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ProductPage/ProductRepository.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; +using CMS.Websites; + +using Kentico.Content.Web.Mvc; + +namespace DancingGoat.Models +{ + /// + /// Repository for managing product-related data retrieval operations. + /// + public class ProductRepository + { + private readonly IContentRetriever contentRetriever; + + + /// + /// Initializes a new instance of the class. + /// + /// The content retriever. + public ProductRepository(IContentRetriever contentRetriever) + { + this.contentRetriever = contentRetriever; + } + + + /// + /// Retrieves products by their content item IDs. + /// + /// The collection of product content item IDs to retrieve. + /// The cancellation token. + public async Task> GetProductsByIds(IEnumerable productIds, CancellationToken cancellationToken = default) + { + var products = await contentRetriever.RetrieveContentOfReusableSchemas( + [IProductFields.REUSABLE_FIELD_SCHEMA_NAME], + new RetrieveContentOfReusableSchemasParameters + { + LinkedItemsMaxLevel = 1, + WorkspaceNames = [DancingGoatConstants.COMMERCE_WORKSPACE_NAME] + }, + query => query.Where(where => where.WhereIn(nameof(IContentQueryDataContainer.ContentItemID), productIds)), + new RetrievalCacheSettings($"WhereIn_{nameof(IContentQueryDataContainer.ContentItemID)}_{string.Join("_", productIds)}"), + cancellationToken + ); + + return products; + } + + + /// + /// Retrieves the URLs of product pages associated with the specified product IDs. + /// + /// The collection of product content item IDs for which to retrieve page URLs. + /// The cancellation token. + public async Task> GetProductPageUrls(IEnumerable productIds, CancellationToken cancellationToken = default) + { + var productPages = await contentRetriever.RetrievePages( + new RetrievePagesParameters + { + LinkedItemsMaxLevel = 1, + PathMatch = PathMatch.Children(DancingGoatConstants.PRODUCTS_PAGE_TREE_PATH) + }, + query => query.Linking(nameof(ProductPage.ProductPageProduct), productIds), + new RetrievalCacheSettings($"Linking_{nameof(ProductPage.ProductPageProduct)}_{string.Join("_", productIds)}"), + cancellationToken + ); + + var productPageUrls = productPages.ToDictionary( + p => p.ProductPageProduct.First().SystemFields.ContentItemID, + p => p.GetUrl().RelativePath + ); + + return productPageUrls; + } + } +} diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/ProductSectionListViewModel.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductSectionListViewModel.cs new file mode 100644 index 0000000..44511ab --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ProductPage/ProductSectionListViewModel.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DancingGoat.Models +{ + public record ProductSectionListViewModel(string Title, IEnumerable Items) + { + } +} diff --git a/examples/DancingGoat/Models/WebPage/ProductPage/ProductViewModel.cs b/examples/DancingGoat/Models/WebPage/ProductPage/ProductViewModel.cs new file mode 100644 index 0000000..a733850 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ProductPage/ProductViewModel.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DancingGoat.Models +{ + public record ProductViewModel(string Name, string Description, string ImagePath, decimal Price, string Tag, int ContentItemId, IDictionary Parameters, IDictionary Variants) + { + } +} diff --git a/examples/DancingGoat/Models/WebPage/ProductsSection/ProductSectionRepository.cs b/examples/DancingGoat/Models/WebPage/ProductsSection/ProductSectionRepository.cs deleted file mode 100644 index f58f027..0000000 --- a/examples/DancingGoat/Models/WebPage/ProductsSection/ProductSectionRepository.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using CMS.ContentEngine; -using CMS.Helpers; -using CMS.Websites; -using CMS.Websites.Routing; - -namespace DancingGoat.Models -{ - public class ProductSectionRepository : ContentRepositoryBase - { - private readonly ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever; - - - public ProductSectionRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache, ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, cache) - { - this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; - } - - - public async Task GetProductsSection(int id, string languageName, CancellationToken cancellationToken = default) - { - var queryBuilder = GetQueryBuilder(id, languageName); - - var cacheSettings = new CacheSettings(5, WebsiteChannelContext.WebsiteChannelName, nameof(ProductsSection), id, languageName); - - var result = await GetCachedQueryResult(queryBuilder, null, cacheSettings, GetDependencyCacheKeys, cancellationToken); - - return result.FirstOrDefault(); - } - - - private Task> GetDependencyCacheKeys(IEnumerable productsSection, CancellationToken cancellationToken) - { - var dependencyCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - foreach (var productSection in productsSection) - { - dependencyCacheKeys.UnionWith(GetDependencyCacheKeys(productSection)); - } - - dependencyCacheKeys.Add(CacheHelper.GetCacheItemName(null, WebsiteChannelInfo.OBJECT_TYPE, "byid", WebsiteChannelContext.WebsiteChannelID)); - - return Task.FromResult>(dependencyCacheKeys); - } - - - private static IEnumerable GetDependencyCacheKeys(ProductsSection productsSection) - { - if (productsSection == null) - { - return Enumerable.Empty(); - } - - var cacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byid", productsSection.SystemFields.WebPageItemID.ToString() }, false), - CacheHelper.BuildCacheItemName(new[] { "webpageitem", "byguid", productsSection.SystemFields.WebPageItemGUID.ToString() }, false), - }; - - return cacheKeys; - } - - - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) - { - return new ContentItemQueryBuilder() - .ForContentType(ProductsSection.CONTENT_TYPE_NAME, - config => - config - .ForWebsite(WebsiteChannelContext.WebsiteChannelName) - .Where(where => where.WhereEquals(nameof(WebPageFields.WebPageItemID), id)) - .TopN(1)) - .InLanguage(languageName); - } - } -} diff --git a/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCart.generated.cs b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCart.generated.cs new file mode 100644 index 0000000..8ff689f --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCart.generated.cs @@ -0,0 +1,37 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; +using CMS.Websites; + +namespace DancingGoat.Models +{ + /// + /// Represents a page of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class ShoppingCart : IWebPageFieldsSource + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.ShoppingCart"; + + + /// + /// Represents system properties for a web page item. + /// + [SystemField] + public WebPageFields SystemFields { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartItemViewModel.cs b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartItemViewModel.cs new file mode 100644 index 0000000..fa29233 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartItemViewModel.cs @@ -0,0 +1,3 @@ +namespace DancingGoat.Models; + +public record ShoppingCartItemViewModel(int ContentItemId, string Name, string ImageUrl, string DetailUrl, int Quantity, decimal UnitPrice, decimal TotalPrice, int? VariantId); diff --git a/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartViewModel.cs b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartViewModel.cs new file mode 100644 index 0000000..d844268 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/ShoppingCart/ShoppingCartViewModel.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace DancingGoat.Models; + +public record ShoppingCartViewModel(ICollection Items, decimal TotalPrice); diff --git a/examples/DancingGoat/Models/WebPage/Store/Store.generated.cs b/examples/DancingGoat/Models/WebPage/Store/Store.generated.cs new file mode 100644 index 0000000..912ef13 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/Store/Store.generated.cs @@ -0,0 +1,55 @@ +//-------------------------------------------------------------------------------------------------- +// +// +// This code was generated by code generator tool. +// +// To customize the code use your own partial class. For more info about how to use and customize +// the generated code see the documentation at https://docs.xperience.io/. +// +// +//-------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using CMS.ContentEngine; +using CMS.Websites; + +namespace DancingGoat.Models +{ + /// + /// Represents a page of type . + /// + [RegisterContentTypeMapping(CONTENT_TYPE_NAME)] + public partial class Store : IWebPageFieldsSource, ISEOFields + { + /// + /// Code name of the content type. + /// + public const string CONTENT_TYPE_NAME = "DancingGoat.Store"; + + + /// + /// Represents system properties for a web page item. + /// + [SystemField] + public WebPageFields SystemFields { get; set; } + + + /// + /// SEOFieldsTitle. + /// + public string SEOFieldsTitle { get; set; } + + + /// + /// SEOFieldsDescription. + /// + public string SEOFieldsDescription { get; set; } + + + /// + /// SEOFieldsAllowSearchIndexing. + /// + public bool SEOFieldsAllowSearchIndexing { get; set; } + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/Store/StoreViewModel.cs b/examples/DancingGoat/Models/WebPage/Store/StoreViewModel.cs new file mode 100644 index 0000000..67f52c8 --- /dev/null +++ b/examples/DancingGoat/Models/WebPage/Store/StoreViewModel.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using CMS.ContentEngine; +using CMS.Websites; + +namespace DancingGoat.Models +{ + public record StoreViewModel(IEnumerable SelectionProductList, IEnumerable CategoryMenuViewModel) : IWebPageBasedViewModel + { + /// + public IWebPageFieldsSource WebPage { get; init; } + + + /// + /// Validates and maps to a . + /// + /// Store page. + /// Products to be displayed. + /// Product page URLs. + /// Tag names that define separate sets of product to be displayed. + /// "Product tags" taxonomy data + /// Language name to map. + /// Category menu view model to map. + public static StoreViewModel GetViewModel(Store store, IEnumerable products, IDictionary productPageUrls, IEnumerable productSectionTagNames, TaxonomyData productTagsTaxonomy, string languageName, IEnumerable categoryMenuViewModel) + { + var productSections = new List(); + + var productSectionTags = productTagsTaxonomy.Tags + .Where(t => productSectionTagNames.Contains(t.Name, StringComparer.InvariantCultureIgnoreCase)) + .OrderBy(t => productSectionTagNames.ToList().IndexOf(t.Name)); + + foreach (var productSectionTag in productSectionTags) + { + productSections.Add(new ProductSectionListViewModel( + productSectionTag.Title, + products + .Where(product => product.ProductFieldTags.Any(t => t.Identifier == productSectionTag.Identifier)) + .Select(product => + { + productPageUrls.TryGetValue((product as IContentItemFieldsSource).SystemFields.ContentItemID, out var pageUrl); + + return new ProductListItemViewModel( + product.ProductFieldName, + product.ProductFieldImage.FirstOrDefault()?.ImageFile.Url, + pageUrl, + product.ProductFieldPrice, + null); + }) + )); + } + + return new StoreViewModel(productSections, categoryMenuViewModel) + { + WebPage = store + }; + } + } +} diff --git a/examples/DancingGoat/PageTemplates/Article/_Article.cshtml b/examples/DancingGoat/PageTemplates/Article/_Article.cshtml index f95d123..ed70e4d 100644 --- a/examples/DancingGoat/PageTemplates/Article/_Article.cshtml +++ b/examples/DancingGoat/PageTemplates/Article/_Article.cshtml @@ -7,7 +7,7 @@ Layout = "~/Views/Shared/_DancingGoatLayout.cshtml"; var viewModel = Model.GetTemplateModel(); - var hasRelatedArticles = viewModel.RelatedArticles.Any(); + var hasRelatedArticles = viewModel.RelatedPages.Any(); } @@ -28,14 +28,14 @@ }
-
+
@Html.Raw(viewModel.Text)
- +
} else diff --git a/examples/DancingGoat/ProductTagTaxonomyConstants.cs b/examples/DancingGoat/ProductTagTaxonomyConstants.cs new file mode 100644 index 0000000..5a12f01 --- /dev/null +++ b/examples/DancingGoat/ProductTagTaxonomyConstants.cs @@ -0,0 +1,16 @@ +namespace DancingGoat +{ + internal static class ProductTagTaxonomyConstants + { + /// + /// Name of the bestseller tag. + /// + public const string TAG_NAME_BESTSELLER = "Bestsellers"; + + + /// + /// Name of the hot tips tag. + /// + public const string TAG_NAME_HOT_TIPS = "HotTips"; + } +} diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index f2c6d93..47188be 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -1,14 +1,24 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using DancingGoat; +using DancingGoat.Commerce; +using DancingGoat.EmailComponents; +using DancingGoat.Helpers.Generators; using DancingGoat.Models; +using CMS; +using CMS.Base; + using Kentico.Activities.Web.Mvc; +using Kentico.Commerce.Web.Mvc; using Kentico.Content.Web.Mvc.Routing; +using Kentico.EmailBuilder.Web.Mvc; using Kentico.Membership; using Kentico.OnlineMarketing.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; +using Kentico.Xperience.Mjml; using Kentico.Web.Mvc; @@ -20,9 +30,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using DancingGoat.TagManager; +using Samples.DancingGoat; + +[assembly: AssemblyDiscoverable] + var builder = WebApplication.CreateBuilder(args); @@ -40,10 +55,14 @@ } }); + features.UseEmailBuilder(); features.UseWebPageRouting(); features.UseEmailMarketing(); features.UseEmailStatisticsLogging(); features.UseActivityTracking(); +#pragma warning disable KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + features.UseCommerce(); +#pragma warning restore KXE0002 // Commerce feature is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. }); builder.Services.Configure(options => options.LowercaseUrls = true); @@ -57,18 +76,27 @@ }); builder.Services.AddDancingGoatServices(); +builder.Services.AddSingleton(); + +ConfigureEmailBuilder(builder.Services); +ConfigureMembershipServices(builder.Services); builder.Services.AddKenticoTagManager(builder.Configuration, builder => { builder.AddSnippetFactory(); }); -ConfigureMembershipServices(builder.Services); +if (builder.Environment.IsDevelopment()) +{ + builder.Services.Configure(options => options.UseSSL = false); +} var app = builder.Build(); app.InitKentico(); +Initialize(app.Services); + app.UseStaticFiles(); app.UseCookiePolicy(); @@ -148,9 +176,34 @@ static void ConfigureMembershipServices(IServiceCollection services) services.Configure(options => { - // The expiration time span of 8 hours is set for demo purposes only. In production environment, set expiration according to best practices. + // The expiration time span of 8 hours is set for demo purposes only. In production environments, set expiration according to best practices. options.AuthenticationOptions.ExpireTimeSpan = TimeSpan.FromHours(8); + + // The forbidden passwords are set for demo purposes only. In production environments, set password options according to best practices. + var companySpecificKeywords = new List { "kentico", "dancinggoat", "admin", "coffee" }; + var specificNumberCombinations = new List { "2023", "23", "2024", "24", "2025", "25" }; + options.PasswordOptions.ForbiddenPasswords = ForbiddenPasswordGenerator.Generate(companySpecificKeywords, specificNumberCombinations); }); services.AddAuthorization(); } + + +static void Initialize(IServiceProvider serviceProvider) +{ + var contentItemEventHandlers = serviceProvider.GetRequiredService(); + contentItemEventHandlers.Initialize(); +} + + +static void ConfigureEmailBuilder(IServiceCollection services) +{ + services.Configure((EmailBuilderOptions options) => + { + options.AllowedEmailContentTypeNames = ["DancingGoat.BuilderEmail"]; + options.RegisterDefaultSection = false; + options.DefaultSectionIdentifier = DancingGoatFullWidthEmailSection.IDENTIFIER; + }); + + services.AddMjmlForEmails(); +} diff --git a/examples/DancingGoat/Properties/launchSettings.json b/examples/DancingGoat/Properties/launchSettings.json index 6e8fc76..b0e4eb4 100644 --- a/examples/DancingGoat/Properties/launchSettings.json +++ b/examples/DancingGoat/Properties/launchSettings.json @@ -1,9 +1,9 @@ { "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:30512", + "applicationUrl": "http://localhost:11558", "sslPort": 0 } }, @@ -18,7 +18,7 @@ "DancingGoat": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:30512", + "applicationUrl": "http://localhost:11558", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/examples/DancingGoat/Resources/SharedResources.es.resx b/examples/DancingGoat/Resources/SharedResources.es.resx index b299f07..2342bb3 100644 --- a/examples/DancingGoat/Resources/SharedResources.es.resx +++ b/examples/DancingGoat/Resources/SharedResources.es.resx @@ -180,8 +180,8 @@ Registrarse - - Artículos relacionados + + Relacionados Revocar diff --git a/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs b/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs index 949089d..20f82f6 100644 --- a/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs +++ b/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs @@ -10,18 +10,15 @@ namespace DancingGoat { /// - /// + /// Retrieves current website channel primary language. /// - public class CurrentWebsiteChannelPrimaryLanguageRetriever : ICurrentWebsiteChannelPrimaryLanguageRetriever + public sealed class CurrentWebsiteChannelPrimaryLanguageRetriever { private readonly IWebsiteChannelContext websiteChannelContext; private readonly IInfoProvider websiteChannelInfoProvider; private readonly IInfoProvider contentLanguageInfoProvider; - /// - /// Initializes an instance of the class. - /// public CurrentWebsiteChannelPrimaryLanguageRetriever( IWebsiteChannelContext websiteChannelContext, IInfoProvider websiteChannelInfoProvider, @@ -32,7 +29,11 @@ public CurrentWebsiteChannelPrimaryLanguageRetriever( this.contentLanguageInfoProvider = contentLanguageInfoProvider; } - /// + + /// + /// Returns language code of the current website channel primary language. + /// + /// Cancellation instruction. public async Task Get(CancellationToken cancellationToken = default) { var websiteChannel = await websiteChannelInfoProvider.GetAsync(websiteChannelContext.WebsiteChannelID, cancellationToken); diff --git a/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs b/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs deleted file mode 100644 index ff1cc86..0000000 --- a/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace DancingGoat -{ - /// - /// Retrieves current website channel primary language. - /// - public interface ICurrentWebsiteChannelPrimaryLanguageRetriever - { - /// - /// Returns language code of the current website channel primary language. - /// - /// Cancellation instruction. - public Task Get(CancellationToken cancellationToken = default); - } -} diff --git a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs index 3abf54e..14d5805 100644 --- a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs +++ b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs @@ -1,12 +1,10 @@ -using DancingGoat.Models; +using DancingGoat.Commerce; +using DancingGoat.Models; +using DancingGoat.Services; using DancingGoat.ViewComponents; -using Kentico.OnlineMarketing.Web.Mvc; - using Microsoft.Extensions.DependencyInjection; -using Samples.DancingGoat; - namespace DancingGoat { public static class IServiceCollectionExtensions @@ -17,30 +15,36 @@ public static class IServiceCollectionExtensions public static void AddDancingGoatServices(this IServiceCollection services) { AddViewComponentServices(services); - AddRepositories(services); + AddCommerceServices(services); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } - private static void AddRepositories(IServiceCollection services) + private static void AddCommerceServices(IServiceCollection services) { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + + // Register extractors for product types + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Register extractors for product type variants + services.AddSingleton(); } diff --git a/examples/DancingGoat/Services/TagTitleRetriever.cs b/examples/DancingGoat/Services/TagTitleRetriever.cs new file mode 100644 index 0000000..3dbb7ff --- /dev/null +++ b/examples/DancingGoat/Services/TagTitleRetriever.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; + +namespace DancingGoat.Services +{ + /// + /// Tag title retriever to get title of a tag. + /// + public sealed class TagTitleRetriever + { + private readonly ITaxonomyRetriever taxonomyRetriever; + + + public TagTitleRetriever(ITaxonomyRetriever taxonomyRetriever) + { + this.taxonomyRetriever = taxonomyRetriever; + } + + + /// + /// Get title of a tag based on the tag identifier. + /// + /// Tag identifier to retrieve from database. + /// Language name. + /// Cancellation token. + /// Received title if tag exists, null otherwise. + public async Task GetTagTitle(Guid tagIdentifier, string languageName, CancellationToken cancellationToken) + { + var tags = await taxonomyRetriever.RetrieveTags([tagIdentifier], languageName, cancellationToken); + return tags.FirstOrDefault()?.Title; + } + } +} diff --git a/examples/DancingGoat/Services/WebPageUrlProvider.cs b/examples/DancingGoat/Services/WebPageUrlProvider.cs new file mode 100644 index 0000000..c45792c --- /dev/null +++ b/examples/DancingGoat/Services/WebPageUrlProvider.cs @@ -0,0 +1,54 @@ +using System.Threading; +using System.Threading.Tasks; + +using CMS.Websites; +using CMS.Websites.Routing; + +using Kentico.Content.Web.Mvc.Routing; + +namespace DancingGoat.Services +{ + /// + /// Provides URLs of the web pages in the Dancing Goat sample application. + /// + public sealed class WebPageUrlProvider + { + private readonly IWebPageUrlRetriever webPageUrlRetriever; + private readonly IWebsiteChannelContext websiteChannelContext; + private readonly IPreferredLanguageRetriever preferredLanguageRetriever; + + + public WebPageUrlProvider(IWebPageUrlRetriever webPageUrlRetriever, IWebsiteChannelContext websiteChannelContext, IPreferredLanguageRetriever preferredLanguageRetriever) + { + this.webPageUrlRetriever = webPageUrlRetriever; + this.websiteChannelContext = websiteChannelContext; + this.preferredLanguageRetriever = preferredLanguageRetriever; + } + + + public async Task StorePageUrl(string languageName = null, CancellationToken cancellationToken = default) + { + return await GetRelativeWebPagePath(DancingGoatConstants.STORE_PAGE_TREE_PATH, languageName, cancellationToken); + } + + + public async Task ShoppingCartPageUrl(string languageName = null, CancellationToken cancellationToken = default) + { + return await GetRelativeWebPagePath(DancingGoatConstants.SHOPPING_CART_PAGE_TREE_PATH, languageName, cancellationToken); + } + + + public async Task CheckoutPageUrl(string languageName = null, CancellationToken cancellationToken = default) + { + return await GetRelativeWebPagePath(DancingGoatConstants.CHECKOUT_PAGE_TREE_PATH, languageName, cancellationToken); + } + + + private async Task GetRelativeWebPagePath(string webPageTreePath, string languageName, CancellationToken cancellationToken) + { + languageName ??= preferredLanguageRetriever.Get(); + + return (await webPageUrlRetriever.Retrieve(webPageTreePath, websiteChannelContext.WebsiteChannelName, languageName, websiteChannelContext.IsPreview, cancellationToken)).RelativePath; + } + } +} diff --git a/examples/DancingGoat/Views/DancingGoatArticle/RelatedArticles.cshtml b/examples/DancingGoat/Views/DancingGoatArticle/RelatedArticles.cshtml deleted file mode 100644 index df1292d..0000000 --- a/examples/DancingGoat/Views/DancingGoatArticle/RelatedArticles.cshtml +++ /dev/null @@ -1,37 +0,0 @@ -@model IEnumerable -@if (Model.Any()) -{ -
-
-

@HtmlLocalizer["Related articles"]

-
-
- @foreach (var article in Model) - { -
-
- @if (!string.IsNullOrEmpty(article.TeaserUrl)) - { - - @article.Title - - } -
-
- @article.PublicationDate.ToString("m") -
-
-

- @article.Title -

-

- @article.Summary -

-
-
-
-
- } -
-
-} \ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatArticle/RelatedPages.cshtml b/examples/DancingGoat/Views/DancingGoatArticle/RelatedPages.cshtml new file mode 100644 index 0000000..c689802 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatArticle/RelatedPages.cshtml @@ -0,0 +1,40 @@ +@model IEnumerable +@if (Model.Any()) +{ +
+
+

@HtmlLocalizer["Related"]

+
+
+ @foreach (var relatedPage in Model) + { +
+
+ @if (!string.IsNullOrEmpty(relatedPage.TeaserUrl)) + { + + @relatedPage.Title + + } +
+ @if(relatedPage.PublicationDate.HasValue) + { +
+ @relatedPage.PublicationDate?.ToString("m") +
+ } +
+

+ @relatedPage.Title +

+

+ @Html.Raw(relatedPage.Summary) +

+
+
+
+
+ } +
+
+} \ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/ConfirmOrder.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/ConfirmOrder.cshtml new file mode 100644 index 0000000..0b76eca --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/ConfirmOrder.cshtml @@ -0,0 +1,10 @@ +@using DancingGoat.Models + +@model ConfirmOrderViewModel + +@{ + ViewBag.Title = HtmlLocalizer["Order created"].Value; + ViewData["PageClass"] = "inverted"; +} + +

@HtmlLocalizer[$"Your order was created. Order number: {Model.OrderNumber}"]

diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerAddressViewModel.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerAddressViewModel.cshtml new file mode 100644 index 0000000..3f8bfe3 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerAddressViewModel.cshtml @@ -0,0 +1,109 @@ +@using DancingGoat.Commerce +@using DancingGoat.Models +@using DancingGoat.Helpers + +@model CustomerAddressViewModel + +

@HtmlLocalizer["Customer address"]

+ +@Html.ValidatedEditorFor(m => m.Line1) +@Html.ValidatedEditorFor(m => m.Line2) +@Html.ValidatedEditorFor(m => m.City) +@Html.ValidatedEditorFor(m => m.PostalCode) + +@using (Html.BeginForm("Index", "DancingGoatCheckout", FormMethod.Post, new { id = "countryForm" })) +{ +
+
+ @Html.LabelFor(m => m.CountryId) +
+
+ @Html.DropDownListFor(m => m.CountryId, Model.Countries, HtmlLocalizer["Select a country"].Value, new { @id = "countryDropdown", data_storage = $"CustomerAddress_{nameof(CustomerAddressViewModel.Country)}" }) +
+
+ @Html.ValidationMessageFor(m => m.CountryId) +
+
+ +
+
+ @Html.LabelFor(m => m.StateId) +
+
+ @Html.DropDownListFor(m => m.StateId, Model.States, HtmlLocalizer["Select a state"].Value, new { @id = "statesDropdown", data_storage = $"CustomerAddress_{nameof(CustomerAddressViewModel.State)}" }) +
+
+ @Html.ValidationMessageFor(m => m.StateId) +
+
+} + + diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerViewModel.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerViewModel.cshtml new file mode 100644 index 0000000..17cb860 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/EditorTemplates/CustomerViewModel.cshtml @@ -0,0 +1,12 @@ +@using DancingGoat.Models +@using DancingGoat.Helpers + +@model CustomerViewModel + +

@HtmlLocalizer["Customer details"]

+ +@Html.ValidatedEditorFor(m => m.FirstName) +@Html.ValidatedEditorFor(m => m.LastName) +@Html.ValidatedEditorFor(m => m.Company) +@Html.ValidatedEditorFor(m => m.Email) +@Html.ValidatedEditorFor(m => m.PhoneNumber) diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/Index.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/Index.cshtml new file mode 100644 index 0000000..1f81e1a --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/Index.cshtml @@ -0,0 +1,16 @@ +@using DancingGoat.Models + +@model CheckoutViewModel + +@{ + ViewData["PageClass"] = "inverted"; +} + +@if (Model.Step == CheckoutStep.CheckoutCustomer) +{ + +} +else if (Model.Step == CheckoutStep.OrderConfirmation) +{ + +} \ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/_CheckoutCustomer.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/_CheckoutCustomer.cshtml new file mode 100644 index 0000000..e78dcc9 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/_CheckoutCustomer.cshtml @@ -0,0 +1,38 @@ +@using DancingGoat.Models +@using DancingGoat.Services + +@inject WebPageUrlProvider webPageUrlProvider + +@model CheckoutViewModel + +@{ + ViewBag.Title = HtmlLocalizer["Customer details"].Value; + + var shoppingCartPageUrl = await webPageUrlProvider.ShoppingCartPageUrl(); +} + +
+
+ + +
+
+
+
+
+ @Html.EditorFor(m => m.Customer) + @Html.EditorFor(m => m.CustomerAddress) +
+
+
+
+

@HtmlLocalizer["Fill in your billing details and proceed to review your order."]

+ +
+
+
+
+
+
diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/_OrderConfirmation.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/_OrderConfirmation.cshtml new file mode 100644 index 0000000..2d2da90 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/_OrderConfirmation.cshtml @@ -0,0 +1,80 @@ +@using DancingGoat.Models + +@using Kentico.Content.Web.Mvc.Routing + +@inject IPreferredLanguageRetriever currentLanguageRetriever + +@model CheckoutViewModel + +@{ + ViewBag.Title = HtmlLocalizer["Order confirmation"].Value; + + var languageName = currentLanguageRetriever.Get(); +} + +
+
+ + +
@HtmlLocalizer["Your order cannot be completed"]
+ +
+
+
+

@HtmlLocalizer["Billing details"]

+ +
@Model.Customer.FirstName @Model.Customer.LastName
+
@Model.Customer.Company
+
@Model.Customer.Email
+
@Model.Customer.PhoneNumber
+ +
+ +
@Model.CustomerAddress.Line1
+
@Model.CustomerAddress.Line2
+
@Model.CustomerAddress.City
+ +
@Model.CustomerAddress.PostalCode @Model.CustomerAddress.State
+
@Model.CustomerAddress.Country
+
+
+
+

@HtmlLocalizer["Ordered items"]

+
+ + +
+
+
+ @{ + var routeData = new Dictionary { { "languageName", languageName } }; +
+
+
+ +
+ + + + +
+
+ } +
+
+
+ + diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartCheckoutCustomerData.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartCheckoutCustomerData.cshtml new file mode 100644 index 0000000..7df7f15 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartCheckoutCustomerData.cshtml @@ -0,0 +1,15 @@ +@using DancingGoat.Models + +@model CheckoutViewModel + + + + + + + + + + + + diff --git a/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartContentPreview.cshtml b/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartContentPreview.cshtml new file mode 100644 index 0000000..bfffc8e --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatCheckout/_ShoppingCartContentPreview.cshtml @@ -0,0 +1,38 @@ +@using CMS.Commerce +@using DancingGoat.Commerce +@using DancingGoat.Models + +@model ShoppingCartViewModel + +@foreach (var cartItem in Model.Items) +{ +
+
+ @if (!string.IsNullOrEmpty(cartItem.ImageUrl)) + { +
+ + @cartItem.Name + +
+ } + +
+ Qty + + + +
+ + @Html.ValidationMessage(cartItem.ContentItemId.ToString(), new { @class = "red"} ) +
+
+ @cartItem.TotalPrice +
+
+
+} diff --git a/examples/DancingGoat/Views/DancingGoatCoffee/Detail.cshtml b/examples/DancingGoat/Views/DancingGoatCoffee/Detail.cshtml deleted file mode 100644 index bf29fe6..0000000 --- a/examples/DancingGoat/Views/DancingGoatCoffee/Detail.cshtml +++ /dev/null @@ -1,41 +0,0 @@ -@using DancingGoat.Models - -@model CoffeeDetailViewModel; - -@{ - ViewData["PageClass"] = "inverted"; - ViewData["Title"] = Model.Name; -} - -
-
-
-
-

@Model.Name

-
-
-
- -
- @foreach (var item in Model.Processing.Union(Model.Tastes)) - { -
- @item.Title -
- } -
- -
-
- @if (!string.IsNullOrEmpty(Model.ImageUrl)) - { -
- @Model.Name -
- } -
- @Html.Raw(Model.Description) -
-
-
-
\ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatConfirmation/Index.cshtml b/examples/DancingGoat/Views/DancingGoatConfirmation/Index.cshtml index 41cd2c1..249b4e1 100644 --- a/examples/DancingGoat/Views/DancingGoatConfirmation/Index.cshtml +++ b/examples/DancingGoat/Views/DancingGoatConfirmation/Index.cshtml @@ -6,7 +6,7 @@

@Model.Header

-
+
@Html.Raw(Model.Content)
diff --git a/examples/DancingGoat/Views/DancingGoatGrinder/Detail.cshtml b/examples/DancingGoat/Views/DancingGoatGrinder/Detail.cshtml deleted file mode 100644 index 67c293f..0000000 --- a/examples/DancingGoat/Views/DancingGoatGrinder/Detail.cshtml +++ /dev/null @@ -1,41 +0,0 @@ -@using DancingGoat.Models - -@model GrinderDetailViewModel; - -@{ - ViewData["PageClass"] = "inverted"; - ViewData["Title"] = Model.Name; -} - -
-
-
-
-

@Model.Name

-
-
-
- -
- @foreach (var item in Model.Manufacturers.Union(Model.Type)) - { -
- @item.Title -
- } -
- -
-
- @if (!string.IsNullOrEmpty(Model.ImageUrl)) - { -
- @Model.Name -
- } -
- @Html.Raw(Model.Description) -
-
-
-
\ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatProductCategory/Index.cshtml b/examples/DancingGoat/Views/DancingGoatProductCategory/Index.cshtml new file mode 100644 index 0000000..74feae1 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatProductCategory/Index.cshtml @@ -0,0 +1,19 @@ +@using DancingGoat.Models + +@model ProductListingViewModel + +@{ + ViewData["PageClass"] = "inverted"; +} + +
+ + @Html.DisplayFor(m => @Model.CategoryMenuViewModel, "ProductCategoryMenu") + + +
+
+ +
+
+
diff --git a/examples/DancingGoat/Views/DancingGoatProductCategory/ProductsList.cshtml b/examples/DancingGoat/Views/DancingGoatProductCategory/ProductsList.cshtml new file mode 100644 index 0000000..68a7334 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatProductCategory/ProductsList.cshtml @@ -0,0 +1,18 @@ +@using DancingGoat.Models + +@model ProductSectionListViewModel + +@if (Model.Items.Any()) +{ + foreach (var product in Model.Items) + { + +
+ @Html.DisplayFor(m => product, "ProductListItem") +
+ } +} +else +{ + +} \ No newline at end of file diff --git a/examples/DancingGoat/Views/DancingGoatProductDetail/Index.cshtml b/examples/DancingGoat/Views/DancingGoatProductDetail/Index.cshtml new file mode 100644 index 0000000..ade5add --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatProductDetail/Index.cshtml @@ -0,0 +1,100 @@ +@using CMS.Commerce +@using DancingGoat.Commerce +@using DancingGoat.Models +@using Kentico.Content.Web.Mvc.Routing + +@inject IPreferredLanguageRetriever currentLanguageRetriever + +@model ProductViewModel + +@{ + ViewData["PageClass"] = "inverted"; + + var languageName = currentLanguageRetriever.Get(); +} + +
+
+
+
+

@Model.Name

+
+
+
+ +
+
+
+ @if (@Model.Tag != null) + { +
@Model.Tag
+ } + + @if (!string.IsNullOrEmpty(Model.ImagePath)) + { +
+ @Model.Name +
+ } +
+ +
+

+ @Html.Raw(Model.Description) +

+ + @if (Model.Parameters.Count > 0) + { +
+

@HtmlLocalizer["Parameters"]

+ @foreach (var parameter in Model.Parameters) + { +
+
@parameter.Key
+
@parameter.Value
+
+ } +
+ } +
+
+ +
+
+
+ @{ + var routeData = new Dictionary { { "languageName", languageName } }; +
+ @if (Model.Variants?.Count > 0) + { +
+ +
+
+ +
+ } +
+ @HtmlLocalizer["Unit price"] + @Model.Price +
+
+ + + + +
+
+ } +
+ +
+
+
+ +
diff --git a/examples/DancingGoat/Views/DancingGoatShoppingCart/Index.cshtml b/examples/DancingGoat/Views/DancingGoatShoppingCart/Index.cshtml new file mode 100644 index 0000000..e5ac572 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatShoppingCart/Index.cshtml @@ -0,0 +1,57 @@ +@using CMS.Websites +@using CMS.Websites.Routing +@using DancingGoat.Models +@using DancingGoat.Services +@using Kentico.Content.Web.Mvc.Routing + +@inject WebPageUrlProvider webPageUrlProvider + +@model ShoppingCartViewModel + +@{ + ViewBag.Title = HtmlLocalizer["Shopping cart"].Value; + ViewData["PageClass"] = "inverted"; + + var checkoutPageUrl = await webPageUrlProvider.CheckoutPageUrl(); + var storePageUrl = await webPageUrlProvider.StorePageUrl(); +} + +
+
+ + +

@HtmlLocalizer["Your shopping cart"]

+
+
+ @if (!Model.Items.Any()) + { + @HtmlLocalizer["Shopping cart is empty"] +
+ } + else + { + + } +
+ +
+ + @if (Model.Items.Any()) + { +
+
+
+
+ +
+ +

@HtmlLocalizer["Review your shopping cart and checkout"]

+ + +
+
+
+ } +
diff --git a/examples/DancingGoat/Views/DancingGoatShoppingCart/_ShoppingCartContentEdit.cshtml b/examples/DancingGoat/Views/DancingGoatShoppingCart/_ShoppingCartContentEdit.cshtml new file mode 100644 index 0000000..78a6218 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatShoppingCart/_ShoppingCartContentEdit.cshtml @@ -0,0 +1,52 @@ +@using CMS.Commerce +@using DancingGoat.Commerce +@using DancingGoat.Models +@using Kentico.Content.Web.Mvc.Routing + +@inject IPreferredLanguageRetriever currentLanguageRetriever + +@model ShoppingCartViewModel + +@{ + var languageName = currentLanguageRetriever.Get(); +} + +@foreach (var cartItem in Model.Items) +{ +
+
+ @if (!string.IsNullOrEmpty(cartItem.ImageUrl)) + { +
+ + @cartItem.Name + +
+ } + + @{ + var routeData = new Dictionary { { "languageName", languageName } }; +
+
+ Qty + + + + + +
+
+ } + + @Html.ValidationMessage(cartItem.ContentItemId.ToString(), new { @class = "red"}) +
+
+ @cartItem.TotalPrice +
+
+
+} diff --git a/examples/DancingGoat/Views/DancingGoatStore/Index.cshtml b/examples/DancingGoat/Views/DancingGoatStore/Index.cshtml new file mode 100644 index 0000000..e8224f9 --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatStore/Index.cshtml @@ -0,0 +1,21 @@ +@using DancingGoat.Models + +@model StoreViewModel + +@{ + ViewData["PageClass"] = "inverted"; +} + + +@Html.DisplayFor(m => @Model.CategoryMenuViewModel, "ProductCategoryMenu") + +
+ @foreach (var productList in Model?.SelectionProductList ?? []) + { +
+
+ +
+
+ } +
diff --git a/examples/DancingGoat/Views/DancingGoatStore/ProductsList.cshtml b/examples/DancingGoat/Views/DancingGoatStore/ProductsList.cshtml new file mode 100644 index 0000000..1aa322c --- /dev/null +++ b/examples/DancingGoat/Views/DancingGoatStore/ProductsList.cshtml @@ -0,0 +1,16 @@ +@using DancingGoat.Models + +@model ProductSectionListViewModel + +@if (Model.Items.Any()) +{ +

@Model.Title

+ + foreach (var product in Model.Items) + { + +
+ @Html.DisplayFor(m => product, "ProductListItem") +
+ } +} \ No newline at end of file diff --git a/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductCategoryMenu.cshtml b/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductCategoryMenu.cshtml new file mode 100644 index 0000000..bb62632 --- /dev/null +++ b/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductCategoryMenu.cshtml @@ -0,0 +1,14 @@ +@using DancingGoat.Models + +@model IEnumerable + + \ No newline at end of file diff --git a/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductListItem.cshtml b/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductListItem.cshtml index 27684d0..0bf164a 100644 --- a/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductListItem.cshtml +++ b/examples/DancingGoat/Views/Shared/DisplayTemplates/ProductListItem.cshtml @@ -1,15 +1,27 @@ -@using DancingGoat.Models +@using CMS.Commerce +@using DancingGoat.Commerce +@using DancingGoat.Models @model ProductListItemViewModel \ No newline at end of file diff --git a/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml b/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml index 31201d1..8ef4e30 100644 --- a/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml +++ b/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml @@ -1,6 +1,8 @@ @using CMS.Websites.Routing @using CMS.Websites +@using DancingGoat.Services @using Kentico.Activities.Web.Mvc +@using Kentico.Content.Web.Mvc @using Kentico.Content.Web.Mvc.PageBuilder @using Kentico.Content.Web.Mvc.Routing @using Kentico.Forms.Web.Mvc.Widgets @@ -9,9 +11,9 @@ @using DancingGoat.Models @using DancingGoat.ViewComponents -@inject IWebPageUrlRetriever webPageUrlRetriever; -@inject IPreferredLanguageRetriever currentLanguageRetriever; +@inject IContentRetriever contentRetriever; @inject IWebsiteChannelContext websiteChannelContext; +@inject WebPageUrlProvider webPageUrlProvider @model object; @@ -19,9 +21,16 @@ const string ENGLISH = "English"; const string ESPANOL = "Español"; - var language = currentLanguageRetriever.Get(); - - var homePageUrl = (await webPageUrlRetriever.Retrieve(DancingGoatConstants.HOME_PAGE_PATH, websiteChannelContext.WebsiteChannelName, language)).RelativePath; + var homePage = (await contentRetriever.RetrievePages( + RetrievePagesParameters.Default, + query => query.UrlPathColumns(), + new RetrievalCacheSettings("UrlPathColumns"), + cancellationToken: Context.RequestAborted + )).FirstOrDefault(); + + var homePageUrl = homePage.GetUrl().RelativePath; + + var shoppingCartPageUrl = await webPageUrlProvider.ShoppingCartPageUrl(); var routeDataLanguage = Convert.ToString(@ViewContext.RouteData.Values[WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY]); var currentLanguage = routeDataLanguage.Equals("es", StringComparison.OrdinalIgnoreCase) ? "ES" : "EN"; @@ -76,6 +85,7 @@